Compare commits

..

54 Commits
0.9.2 ... nbc

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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

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


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


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


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


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


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

19
.gitignore vendored
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

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

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -20,7 +20,24 @@ class AccountController < ApplicationController
include CustomFieldsHelper
# prevents login action to be filtered by check_if_login_required application scope filter
skip_before_filter :check_if_login_required
skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
# Show user's account
def show
@user = User.active.find(params[:id])
@custom_values = @user.custom_values
# show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership|
membership.project.is_public? || (User.current.member_of?(membership.project))
end
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
@events_by_day = events.group_by(&:event_date)
rescue ActiveRecord::RecordNotFound
render_404
end
# Login request and validation
def login
@@ -67,9 +84,9 @@ class AccountController < ApplicationController
if request.post?
user = User.find_by_mail(params[:mail])
# user not found in db
(flash.now[:error] = l(:notice_account_unknown_email); return) unless user
flash.now[:error] = l(:notice_account_unknown_email) and return unless user
# user uses an external authentification
(flash.now[:error] = l(:notice_can_t_change_password); return) if user.auth_source_id
flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id
# create a new token for password recovery
token = Token.new(:user => user, :action => "recovery")
if token.save
@@ -133,15 +150,27 @@ class AccountController < ApplicationController
redirect_to :action => 'login'
end
private
private
def logged_user=(user)
if user && user.is_a?(User)
User.current = user
session[:user_id] = user.id
else
User.current = User.anonymous
session[:user_id] = nil
end
end
def password_authentication
user = User.try_to_login(params[:username], params[:password])
if user.nil?
invalid_credentials
# Invalid credentials
flash.now[:error] = l(:notice_account_invalid_creditentials)
elsif user.new_record?
onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id })
# Onthefly creation failed, display the registration form to fill/fix attributes
@user = user
session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id }
render :action => 'register'
else
# Valid user
successful_authentication(user)
@@ -167,24 +196,20 @@ class AccountController < ApplicationController
case Setting.self_registration
when '1'
register_by_email_activation(user) do
onthefly_creation_failed(user)
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end
when '3'
register_automatically(user) do
onthefly_creation_failed(user)
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end
else
register_manually_by_administrator(user) do
onthefly_creation_failed(user)
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end
end
else
# Existing record
if user.active?
successful_authentication(user)
else
account_pending
end
successful_authentication(user)
end
end
end
@@ -198,7 +223,6 @@ class AccountController < ApplicationController
token = Token.create(:user => user, :action => 'autologin')
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
end
call_hook(:controller_account_success_authentication_after, {:user => user })
redirect_back_or_default :controller => 'my', :action => 'page'
end
@@ -209,10 +233,6 @@ class AccountController < ApplicationController
render :action => 'register'
end
def invalid_credentials
flash.now[:error] = l(:notice_account_invalid_creditentials)
end
# Register a user for email activation.
#
# Pass a block for behavior when a user fails to save
@@ -233,7 +253,6 @@ class AccountController < ApplicationController
def register_automatically(user, &block)
# Automatic activation
user.status = User::STATUS_ACTIVE
user.last_login_on = Time.now
if user.save
self.logged_user = user
flash[:notice] = l(:notice_account_activated)
@@ -250,14 +269,10 @@ class AccountController < ApplicationController
if user.save
# Sends an email to the administrators
Mailer.deliver_account_activation_request(user)
account_pending
flash[:notice] = l(:notice_account_pending)
redirect_to :action => 'login'
else
yield if block_given?
end
end
def account_pending
flash[:notice] = l(:notice_account_pending)
redirect_to :action => 'login'
end
end

View File

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

View File

@@ -19,36 +19,22 @@ require 'uri'
require 'cgi'
class ApplicationController < ActionController::Base
include Redmine::I18n
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
filter_parameter_logging :password
protect_from_forgery
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
include Redmine::Search::Controller
include Redmine::MenuManager::MenuController
helper Redmine::MenuManager::MenuHelper
REDMINE_SUPPORTED_SCM.each do |scm|
require_dependency "repository/#{scm.underscore}"
end
def current_role
@current_role ||= User.current.role_for_project(@project)
end
def user_setup
# Check the settings cache for each request
Setting.check_cache
@@ -57,40 +43,16 @@ class ApplicationController < ActionController::Base
end
# Returns the current user or nil if no user is logged in
# and starts a session if needed
def find_current_user
if session[:user_id]
# existing session
(User.active.find(session[:user_id]) rescue nil)
elsif cookies[:autologin] && Setting.autologin?
# auto-login feature starts a new session
user = User.try_to_autologin(cookies[:autologin])
session[:user_id] = user.id if user
user
elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication does not start a session
# auto-login feature
User.find_by_autologin_key(cookies[:autologin])
elsif params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication
User.find_by_rss_key(params[:key])
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
@@ -102,34 +64,25 @@ class ApplicationController < ActionController::Base
end
def set_localization
lang = nil
if User.current.logged?
lang = find_language(User.current.language)
end
if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
if !accept_lang.blank?
lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
User.current.language = nil unless User.current.logged?
lang = begin
if !User.current.language.blank? && GLoc.valid_language?(User.current.language)
User.current.language
elsif request.env['HTTP_ACCEPT_LANGUAGE']
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first))
User.current.language = accept_lang
end
end
end
lang ||= Setting.default_language
set_language_if_valid(lang)
rescue
nil
end || Setting.default_language
set_language_if_valid(lang)
end
def require_login
if !User.current.logged?
# Extract only the basic url parameters on non-GET requests
if request.get?
url = url_for(params)
else
url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
end
respond_to do |format|
format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
format.xml { head :unauthorized }
format.json { head :unauthorized }
end
redirect_to :controller => "account", :action => "login", :back_url => url_for(params)
return false
end
true
@@ -149,15 +102,10 @@ class ApplicationController < ActionController::Base
end
# Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action], global = false)
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
def authorize(ctrl = params[:controller], action = params[:action])
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
allowed ? true : deny_access
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
# used as a before_filter for actions that do not require any particular permission on the project
@@ -182,8 +130,7 @@ class ApplicationController < ActionController::Base
uri = URI.parse(back_url)
# do not redirect user to another host or to the login or register page
if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
redirect_to(back_url)
return
redirect_to(back_url) and return
end
rescue URI::InvalidURIError
# redirect to default
@@ -194,7 +141,7 @@ class ApplicationController < ActionController::Base
def render_403
@project = nil
render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403
render :template => "common/403", :layout => !request.xhr?, :status => 403
return false
end
@@ -205,11 +152,7 @@ class ApplicationController < ActionController::Base
def render_error(msg)
flash.now[:error] = msg
render :text => '', :layout => !request.xhr?, :status => 500
end
def invalid_authenticity_token
render_error "Invalid form authenticity token."
render :nothing => true, :layout => !request.xhr?, :status => 500
end
def render_feed(items, options={})
@@ -282,8 +225,6 @@ class ApplicationController < ActionController::Base
tmp.collect!{|val, q| val}
end
return tmp
rescue
nil
end
# Returns a string that can be used as filename value in Content-Disposition header

View File

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

View File

@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AuthSourcesController < ApplicationController
layout 'admin'
before_filter :require_admin
def index

View File

@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class BoardsController < ApplicationController
default_search_scope :messages
before_filter :find_project, :authorize
helper :messages
@@ -36,29 +35,18 @@ class BoardsController < ApplicationController
end
def show
respond_to do |format|
format.html {
sort_init 'updated_on', 'desc'
sort_update 'created_on' => "#{Message.table_name}.created_on",
'replies' => "#{Message.table_name}.replies_count",
'updated_on' => "#{Message.table_name}.updated_on"
@topic_count = @board.topics.count
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
@topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
:include => [:author, {:last_reply => :author}],
:limit => @topic_pages.items_per_page,
:offset => @topic_pages.current.offset
@message = Message.new
render :action => 'show', :layout => !request.xhr?
}
format.atom {
@messages = @board.messages.find :all, :order => 'created_on DESC',
:include => [:author, :board],
:limit => Setting.feeds_limit.to_i
render_feed(@messages, :title => "#{@project}: #{@board}")
}
end
sort_init 'updated_on', 'desc'
sort_update 'created_on' => "#{Message.table_name}.created_on",
'replies' => "#{Message.table_name}.replies_count",
'updated_on' => "#{Message.table_name}.updated_on"
@topic_count = @board.topics.count
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
@topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
:include => [:author, {:last_reply => :author}],
:limit => @topic_pages.items_per_page,
:offset => @topic_pages.current.offset
render :action => 'show', :layout => !request.xhr?
end
verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
@@ -74,6 +62,12 @@ class BoardsController < ApplicationController
def edit
if request.post? && @board.update_attributes(params[:board])
case params[:position]
when 'highest'; @board.move_to_top
when 'higher'; @board.move_higher
when 'lower'; @board.move_lower
when 'lowest'; @board.move_to_bottom
end if params[:position]
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end
end

View File

@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class CustomFieldsController < ApplicationController
layout 'admin'
before_filter :require_admin
def index
@@ -32,11 +30,10 @@ class CustomFieldsController < ApplicationController
end
rescue
end
(redirect_to(:action => 'index'); return) unless @custom_field.is_a?(CustomField)
redirect_to(:action => 'index') and return unless @custom_field.is_a?(CustomField)
if request.post? and @custom_field.save
flash[:notice] = l(:notice_successful_create)
call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
redirect_to :action => 'index', :tab => @custom_field.class.name
end
@trackers = Tracker.find(:all, :order => 'position')
@@ -46,11 +43,25 @@ class CustomFieldsController < ApplicationController
@custom_field = CustomField.find(params[:id])
if request.post? and @custom_field.update_attributes(params[:custom_field])
flash[:notice] = l(:notice_successful_update)
call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
redirect_to :action => 'index', :tab => @custom_field.class.name
end
@trackers = Tracker.find(:all, :order => 'position')
end
def move
@custom_field = CustomField.find(params[:id])
case params[:position]
when 'highest'
@custom_field.move_to_top
when 'higher'
@custom_field.move_higher
when 'lower'
@custom_field.move_lower
when 'lowest'
@custom_field.move_to_bottom
end if params[:position]
redirect_to :action => 'index', :tab => @custom_field.class.name
end
def destroy
@custom_field = CustomField.find(params[:id]).destroy

View File

@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class DocumentsController < ApplicationController
default_search_scope :documents
before_filter :find_project, :only => [:index, :new]
before_filter :find_document, :except => [:index, :new]
before_filter :authorize
@@ -28,7 +27,7 @@ class DocumentsController < ApplicationController
documents = @project.documents.find :all, :include => [:attachments, :category]
case @sort_by
when 'date'
@grouped = documents.group_by {|d| d.updated_on.to_date }
@grouped = documents.group_by {|d| d.created_on.to_date }
when 'title'
@grouped = documents.group_by {|d| d.title.first.upcase}
when 'author'
@@ -49,12 +48,13 @@ class DocumentsController < ApplicationController
if request.post? and @document.save
attach_files(@document, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
redirect_to :action => 'index', :project_id => @project
end
end
def edit
@categories = DocumentCategory.all
@categories = Enumeration::get_values('DCAT')
if request.post? and @document.update_attributes(params[:document])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @document

View File

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

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

View File

@@ -16,11 +16,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueStatusesController < ApplicationController
layout 'admin'
before_filter :require_admin
verify :method => :post, :only => [ :destroy, :create, :update, :move, :update_issue_done_ratio ],
verify :method => :post, :only => [ :destroy, :create, :update, :move ],
:redirect_to => { :action => :list }
def index
@@ -60,6 +58,21 @@ class IssueStatusesController < ApplicationController
render :action => 'edit'
end
end
def move
@issue_status = IssueStatus.find(params[:id])
case params[:position]
when 'highest'
@issue_status.move_to_top
when 'higher'
@issue_status.move_higher
when 'lower'
@issue_status.move_lower
when 'lowest'
@issue_status.move_to_bottom
end if params[:position]
redirect_to :action => 'list'
end
def destroy
IssueStatus.find(params[:id]).destroy
@@ -68,13 +81,4 @@ class IssueStatusesController < ApplicationController
flash[:error] = "Unable to delete issue status"
redirect_to :action => 'list'
end
def update_issue_done_ratio
if IssueStatus.update_issue_done_ratios
flash[:notice] = l(:notice_issue_done_ratios_updated)
else
flash[:error] = l(:error_issue_done_ratios_not_updated)
end
redirect_to :action => 'list'
end
end

View File

@@ -17,17 +17,14 @@
class IssuesController < ApplicationController
menu_item :new_issue, :only => :new
default_search_scope :issues
before_filter :find_issue, :only => [:show, :edit, :reply]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :update_form, :preview]
before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :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]
accept_key_auth :index, :show, :changes
accept_key_auth :index, :changes
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :journals
helper :projects
include ProjectsHelper
@@ -40,45 +37,37 @@ class IssuesController < ApplicationController
helper :attachments
include AttachmentsHelper
helper :queries
include QueriesHelper
helper :sort
include SortHelper
include IssuesHelper
helper :timelog
include Redmine::Export::PDF
verify :method => :post,
:only => :destroy,
:render => { :nothing => true, :status => :method_not_allowed }
def index
retrieve_query
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
sort_init 'id', 'desc'
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
if @query.valid?
limit = case params[:format]
when 'csv', 'pdf'
Setting.issues_export_limit.to_i
when 'atom'
Setting.feeds_limit.to_i
else
per_page_option
limit = per_page_option
respond_to do |format|
format.html { }
format.atom { }
format.csv { limit = Setting.issues_export_limit.to_i }
format.pdf { limit = Setting.issues_export_limit.to_i }
end
@issue_count = @query.issue_count
@issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
@issue_pages = Paginator.new self, @issue_count, limit, params['page']
@issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
:order => sort_clause,
:offset => @issue_pages.current.offset,
:limit => limit)
@issue_count_by_group = @query.issue_count_by_group
@issues = Issue.find :all, :order => sort_clause,
:include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
:conditions => @query.statement,
:limit => limit,
:offset => @issue_pages.current.offset
respond_to do |format|
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
end
else
# Send html if the query is not valid
@@ -91,11 +80,13 @@ class IssuesController < ApplicationController
def changes
retrieve_query
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?
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
:limit => 25)
@journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
:conditions => @query.statement,
:limit => 25,
:order => "#{Journal.table_name}.created_on DESC"
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
@@ -107,11 +98,9 @@ class IssuesController < ApplicationController
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
@journals.each_with_index {|j,i| j.indice = i+1}
@journals.reverse! if User.current.wants_comments_in_reverse_order?
@changesets = @issue.changesets.visible.all
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@priorities = IssuePriority.all
@priorities = Enumeration::get_values('IPRI')
@time_entry = TimeEntry.new
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
@@ -129,7 +118,8 @@ class IssuesController < ApplicationController
# Tracker must be set before custom field values
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
if @issue.tracker.nil?
render_error l(:error_no_tracker_in_project)
flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
render :nothing => true, :layout => true
return
end
if params[:issue].is_a?(Hash)
@@ -140,11 +130,12 @@ class IssuesController < ApplicationController
default_status = IssueStatus.default
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
end
@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?
@issue.start_date ||= Date.today
@@ -152,17 +143,17 @@ class IssuesController < ApplicationController
requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
# Check that the user is allowed to apply the requested status
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
if @issue.save
attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
{ :action => 'show', :id => @issue })
return
end
end
@priorities = IssuePriority.all
@priorities = Enumeration::get_values('IPRI')
render :layout => !request.xhr?
end
@@ -172,7 +163,7 @@ class IssuesController < ApplicationController
def edit
@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)
@time_entry = TimeEntry.new
@@ -189,29 +180,28 @@ class IssuesController < ApplicationController
if request.post?
@time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
if @issue.save
# Log spend time
if User.current.allowed_to?(:log_time, @project)
@time_entry.save
end
if !journal.new_record?
# Only send notification if something was actually changed
flash[:notice] = l(:notice_successful_update)
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})
attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
# Log spend time
if User.current.allowed_to?(:log_time, @project)
@time_entry.save
end
if !journal.new_record?
# Only send notification if something was actually changed
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
end
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash.now[:error] = l(:notice_locking_conflict)
# Remove the previously added attachments if issue was not updated
attachments.each(&:destroy)
end
def reply
@@ -237,18 +227,16 @@ class IssuesController < ApplicationController
# Bulk edit a set of issues
def bulk_edit
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])
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])
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
unsaved_issue_ids = []
@issues.each do |issue|
journal = issue.init_journal(User.current, params[:notes])
issue.tracker = tracker if tracker
issue.priority = priority if priority
issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
issue.category = category if category || params[:category_id] == 'none'
@@ -259,7 +247,10 @@ class IssuesController < ApplicationController
issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
# Don't save any change to the issue if the user is not authorized to apply the requested status
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
unsaved_issue_ids << issue.id
end
@@ -267,65 +258,42 @@ class IssuesController < ApplicationController
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
:total => @issues.size,
:ids => '#' + unsaved_issue_ids.join(', #'))
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
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
end
@available_statuses = Workflow.available_statuses(@project)
@custom_fields = @project.all_issue_custom_fields
# Find potential statuses the user could be allowed to switch issues to
@available_statuses = Workflow.find(:all, :include => :new_status,
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
@custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
end
def move
@copy = params[:copy_options] && params[:copy_options][:copy]
@allowed_projects = []
# find projects to which the user is allowed to move the issue
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
@allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
else
User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
end
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
@available_statuses = Workflow.available_statuses(@project)
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
moved_issues = []
@issues.each do |issue|
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)
if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
moved_issues << r
else
unsaved_issue_ids << issue.id
end
unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
: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
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
render :layout => false if request.xhr?
@@ -359,31 +327,31 @@ class IssuesController < ApplicationController
def gantt
@gantt = Redmine::Helpers::Gantt.new(params)
retrieve_query
@query.group_by = nil
if @query.valid?
events = []
# Issues that have start and due dates
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
: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]
)
events += Issue.find(:all,
:order => "start_date, due_date",
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Issues that don't have a due date but that are assigned to a version with a date
events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
: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]
)
events += Issue.find(:all,
:order => "start_date, effective_date",
:include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
:conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Versions
events += @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
end
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
respond_to do |format|
format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
end
end
@@ -399,13 +367,14 @@ class IssuesController < ApplicationController
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
retrieve_query
@query.group_by = nil
if @query.valid?
events = []
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
:conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
)
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
events += Issue.find(:all,
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
)
events += Version.find(:all, :include => :project,
:conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
@calendar.events = events
end
@@ -432,28 +401,18 @@ class IssuesController < ApplicationController
if @project
@assignables = @project.assignable_users
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
@trackers = @project.trackers
end
@priorities = IssuePriority.all.reverse
@priorities = Enumeration.get_values('IPRI').reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@back = params[:back_url] || request.env['HTTP_REFERER']
@back = request.env['HTTP_REFERER']
render :layout => false
end
def update_form
if params[:id].blank?
@issue = Issue.new
@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'
@issue = Issue.new(params[:issue])
render :action => :new, :layout => false
end
def preview
@@ -480,8 +439,7 @@ private
@project = projects.first
else
# TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects'
return false
render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
end
rescue ActiveRecord::RecordNotFound
render_404
@@ -509,7 +467,6 @@ private
@query = Query.find(params[:query_id], :conditions => cond)
@query.project = @project
session[:query] = {:id => @query.id, :project_id => @query.project_id}
sort_clear
else
if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid
@@ -524,22 +481,12 @@ private
@query.add_short_filter(field, params[field]) if params[field]
end
end
@query.group_by = params[:group_by]
@query.column_names = params[:query] && params[:query][:column_names]
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :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
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

View File

@@ -33,7 +33,7 @@ class JournalsController < ApplicationController
private
def find_journal
@journal = Journal.find(params[:id])
(render_403; return false) unless @journal.editable_by?(User.current)
render_403 and return false unless @journal.editable_by?(User.current)
@project = @journal.journalized.project
rescue ActiveRecord::RecordNotFound
render_404

View File

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

View File

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

View File

@@ -17,7 +17,6 @@
class MessagesController < ApplicationController
menu_item :boards
default_search_scope :messages
before_filter :find_board, :only => [:new, :preview]
before_filter :find_message, :except => [:new, :preview]
before_filter :authorize, :except => [:preview, :edit, :destroy]
@@ -47,7 +46,6 @@ class MessagesController < ApplicationController
@message.sticky = params[:message]['sticky']
end
if request.post? && @message.save
call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
attach_files(@message, params[:attachments])
redirect_to :action => 'show', :id => @message
end
@@ -60,7 +58,6 @@ class MessagesController < ApplicationController
@reply.board = @board
@topic.children << @reply
if !@reply.new_record?
call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
attach_files(@reply, params[:attachments])
end
redirect_to :action => 'show', :id => @topic
@@ -68,7 +65,7 @@ class MessagesController < ApplicationController
# Edit a message
def edit
(render_403; return false) unless @message.editable_by?(User.current)
render_403 and return false unless @message.editable_by?(User.current)
if params[:message]
@message.locked = params[:message]['locked']
@message.sticky = params[:message]['sticky']
@@ -76,14 +73,13 @@ class MessagesController < ApplicationController
if request.post? && @message.update_attributes(params[:message])
attach_files(@message, params[:attachments])
flash[:notice] = l(:notice_successful_update)
@message.reload
redirect_to :action => 'show', :board_id => @message.board, :id => @message.root
redirect_to :action => 'show', :id => @topic
end
end
# Delete a messages
def destroy
(render_403; return false) unless @message.destroyable_by?(User.current)
render_403 and return false unless @message.destroyable_by?(User.current)
@message.destroy
redirect_to @message.parent.nil? ?
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
@@ -93,12 +89,9 @@ class MessagesController < ApplicationController
def quote
user = @message.author
text = @message.content
subject = @message.subject.gsub('"', '\"')
subject = "RE: #{subject}" unless subject.starts_with?('RE:')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
render(:update) { |page|
page << "$('reply_subject').value = \"#{subject}\";"
page.<< "$('message_content').value = \"#{content}\";"
page.show 'reply'
page << "Form.Element.focus('message_content');"

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -28,13 +28,14 @@ class MyController < ApplicationController
'calendar' => :label_calendar,
'documents' => :label_document_plural,
'timelog' => :label_spent_time
}.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
}.freeze
DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
'right' => ['issuesreportedbyme']
}.freeze
verify :xhr => true,
:session => :page_layout,
:only => [:add_block, :remove_block, :order_blocks]
def index
@@ -77,11 +78,7 @@ class MyController < ApplicationController
# Manage user's password
def password
@user = User.current
if @user.auth_source_id
flash[:error] = l(:notice_can_t_change_password)
redirect_to :action => 'account'
return
end
flash[:error] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id
if request.post?
if @user.check_password?(params[:password])
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
@@ -97,65 +94,43 @@ class MyController < ApplicationController
# Create a new feeds key
def reset_rss_key
if request.post?
if User.current.rss_token
User.current.rss_token.destroy
User.current.reload
end
User.current.rss_key
if request.post? && User.current.rss_token
User.current.rss_token.destroy
flash[:notice] = l(:notice_feeds_access_key_reseted)
end
redirect_to :action => 'account'
end
# Create a new API key
def reset_api_key
if request.post?
if User.current.api_token
User.current.api_token.destroy
User.current.reload
end
User.current.api_key
flash[:notice] = l(:notice_api_access_key_reseted)
end
redirect_to :action => 'account'
end
# User's page layout configuration
def page_layout
@user = User.current
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
session[:page_layout] = @blocks
%w(top left right).each {|f| session[:page_layout][f] ||= [] }
@block_options = []
BLOCKS.each {|k, v| @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]}
BLOCKS.each {|k, v| @block_options << [l(v), k]}
end
# Add a block to user's page
# The block is added on top of the page
# params[:block] : id of the block to add
def add_block
block = params[:block].to_s.underscore
(render :nothing => true; return) unless block && (BLOCKS.keys.include? block)
block = params[:block]
render(:nothing => true) and return unless block && (BLOCKS.keys.include? block)
@user = User.current
layout = @user.pref[:my_page_layout] || {}
# remove if already present in a group
%w(top left right).each {|f| (layout[f] ||= []).delete block }
%w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block }
# add it on top
layout['top'].unshift block
@user.pref[:my_page_layout] = layout
@user.pref.save
session[:page_layout]['top'].unshift block
render :partial => "block", :locals => {:user => @user, :block_name => block}
end
# Remove a block to user's page
# params[:block] : id of the block to remove
def remove_block
block = params[:block].to_s.underscore
@user = User.current
block = params[:block]
# remove block in all groups
layout = @user.pref[:my_page_layout] || {}
%w(top left right).each {|f| (layout[f] ||= []).delete block }
@user.pref[:my_page_layout] = layout
@user.pref.save
%w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block }
render :nothing => true
end
@@ -164,20 +139,23 @@ class MyController < ApplicationController
# params[:list-(top|left|right)] : array of block ids of the group
def order_blocks
group = params[:group]
@user = User.current
if group.is_a?(String)
group_items = (params["list-#{group}"] || []).collect(&:underscore)
if group_items and group_items.is_a? Array
layout = @user.pref[:my_page_layout] || {}
# remove group blocks if they are presents in other groups
%w(top left right).each {|f|
layout[f] = (layout[f] || []) - group_items
}
layout[group] = group_items
@user.pref[:my_page_layout] = layout
@user.pref.save
end
group_items = params["list-#{group}"]
if group_items and group_items.is_a? Array
# remove group blocks if they are presents in other groups
%w(top left right).each {|f|
session[:page_layout][f] = (session[:page_layout][f] || []) - group_items
}
session[:page_layout][group] = group_items
end
render :nothing => true
end
# Save user's page layout
def page_layout_save
@user = User.current
@user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout]
@user.pref.save
session[:page_layout] = nil
redirect_to :action => 'page'
end
end

View File

@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class NewsController < ApplicationController
default_search_scope :news
before_filter :find_news, :except => [:new, :index, :preview]
before_filter :find_project, :only => [:new, :preview]
before_filter :authorize, :except => [:index, :preview]
@@ -26,13 +25,11 @@ class NewsController < ApplicationController
def index
@news_pages, @newss = paginate :news,
: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],
:order => "#{News.table_name}.created_on DESC"
respond_to do |format|
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)}") }
end
end
@@ -48,6 +45,7 @@ class NewsController < ApplicationController
@news.attributes = params[:news]
if @news.save
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
redirect_to :controller => 'news', :action => 'index', :project_id => @project
end
end
@@ -67,7 +65,6 @@ class NewsController < ApplicationController
flash[:notice] = l(:label_comment_added)
redirect_to :action => 'show', :id => @news
else
show
render :action => 'show'
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -21,12 +21,12 @@ class ProjectsController < ApplicationController
menu_item :roadmap, :only => :roadmap
menu_item :files, :only => [:list_files, :add_file]
menu_item :settings, :only => :settings
menu_item :issues, :only => [:changelog]
before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
before_filter :find_project, :except => [ :index, :list, :add, :activity ]
before_filter :find_optional_project, :only => :activity
before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
before_filter :authorize_global, :only => :add
before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
accept_key_auth :activity
after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
@@ -70,55 +70,16 @@ class ProjectsController < ApplicationController
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
@project.trackers = Tracker.all
@project.is_public = Setting.default_projects_public?
@project.enabled_module_names = Setting.default_projects_modules
@project.enabled_module_names = Redmine::AccessControl.available_project_modules
else
@project.enabled_module_names = params[:enabled_modules]
if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
# Add current user as a project member if he is not admin
unless User.current.admin?
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
m = Member.new(:user => User.current, :roles => [r])
@project.members << m
end
if @project.save
@project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :id => @project
end
redirect_to :controller => 'admin', :action => 'projects'
end
end
end
def copy
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all
@root_projects = Project.find(:all,
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
:order => 'name')
@source_project = Project.find(params[:id])
if request.get?
@project = Project.copy_from(@source_project)
if @project
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
else
redirect_to :controller => 'admin', :action => 'projects'
end
else
@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
rescue ActiveRecord::RecordNotFound
redirect_to :controller => 'admin', :action => 'projects'
end
# Show @project
def show
@@ -127,8 +88,9 @@ class ProjectsController < ApplicationController
redirect_to_project_menu_item(@project, params[:jump]) && return
end
@users_by_role = @project.users_by_role
@members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
@subprojects = @project.children.visible
@ancestors = @project.ancestors.visible
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@trackers = @project.rolled_up_trackers
@@ -162,8 +124,8 @@ class ProjectsController < ApplicationController
def edit
if request.post?
@project.attributes = params[:project]
if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
if @project.save
@project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project
else
@@ -179,17 +141,13 @@ class ProjectsController < ApplicationController
end
def archive
if request.post?
unless @project.archive
flash[:error] = l(:error_can_not_archive_project)
end
end
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
@project.archive if request.post? && @project.active?
redirect_to :controller => 'admin', :action => 'projects'
end
def unarchive
@project.unarchive if request.post? && !@project.active?
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
redirect_to :controller => 'admin', :action => 'projects'
end
# Delete @project
@@ -206,26 +164,17 @@ class ProjectsController < ApplicationController
# Add a new issue category to @project
def add_issue_category
@category = @project.issue_categories.build(params[:category])
if request.post?
if @category.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :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
if request.post? and @category.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'categories', :id => @project
end
else
respond_to do |format|
format.html
format.js do
render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) }
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
@@ -233,34 +182,10 @@ class ProjectsController < ApplicationController
# Add a new version to @project
def add_version
@version = @project.versions.build
if params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
@version.attributes = attributes
end
if request.post?
if @version.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :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
@version = @project.versions.build(params[:version])
if request.post? and @version.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'versions', :id => @project
end
end
@@ -276,25 +201,6 @@ class ProjectsController < ApplicationController
end
@versions = @project.versions.sort
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
sort_init 'filename', 'asc'
@@ -307,31 +213,19 @@ class ProjectsController < ApplicationController
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
end
# Show changelog for @project
def changelog
@trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
retrieve_selected_tracker_ids(@trackers)
@versions = @project.versions.sort
end
def roadmap
@trackers = @project.trackers.find(:all, :order => 'position')
retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
@versions = @project.shared_versions.sort
@versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
@issues_by_version = {}
unless @selected_tracker_ids.empty?
@versions.each do |version|
conditions = {:tracker_id => @selected_tracker_ids}
if !@project.versions.include?(version)
conditions.merge!(:project_id => project_ids)
end
issues = version.fixed_issues.visible.find(:all,
:include => [:project, :status, :tracker, :priority],
:conditions => conditions,
: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?}
@trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
retrieve_selected_tracker_ids(@trackers)
@versions = @project.versions.sort
@versions = @versions.select {|v| !v.completed? } unless params[:completed]
end
def activity
@@ -354,22 +248,20 @@ class ProjectsController < ApplicationController
events = @activity.events(@date_from, @date_to)
if events.empty? || stale?(:etag => [events.first, User.current])
respond_to do |format|
format.html {
@events_by_day = events.group_by(&:event_date)
render :layout => false if request.xhr?
}
format.atom {
title = l(:label_activity)
if @author
title = @author.name
elsif @activity.scope.size == 1
title = l("label_#{@activity.scope.first.singularize}_plural")
end
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
}
end
respond_to do |format|
format.html {
@events_by_day = events.group_by(&:event_date)
render :layout => false if request.xhr?
}
format.atom {
title = l(:label_activity)
if @author
title = @author.name
elsif @activity.scope.size == 1
title = l("label_#{@activity.scope.first.singularize}_plural")
end
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
}
end
rescue ActiveRecord::RecordNotFound
@@ -394,26 +286,11 @@ private
render_404
end
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
def retrieve_selected_tracker_ids(selectable_trackers)
if ids = params[:tracker_ids]
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
else
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
@selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
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

View File

@@ -24,13 +24,12 @@ class QueriesController < ApplicationController
@query = Query.new(params[:query])
@query.project = params[:query_is_for_all] ? nil : @project
@query.user = User.current
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
@query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns]
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
@query.group_by ||= params[:group_by]
if request.post? && params[:confirm] && @query.save
flash[:notice] = l(:notice_successful_create)
@@ -48,7 +47,7 @@ class QueriesController < ApplicationController
end if params[:fields]
@query.attributes = params[:query]
@query.project = nil if params[:query_is_for_all]
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
@query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns]
if @query.save

View File

@@ -31,13 +31,13 @@ class ReportsController < ApplicationController
render :template => "reports/issue_report_details"
when "version"
@field = "fixed_version_id"
@rows = @project.shared_versions.sort
@rows = @project.versions.sort
@data = issues_by_version
@report_title = l(:field_version)
render :template => "reports/issue_report_details"
when "priority"
@field = "priority_id"
@rows = IssuePriority.all
@rows = Enumeration::get_values('IPRI')
@data = issues_by_priority
@report_title = l(:field_priority)
render :template => "reports/issue_report_details"
@@ -49,13 +49,13 @@ class ReportsController < ApplicationController
render :template => "reports/issue_report_details"
when "assigned_to"
@field = "assigned_to_id"
@rows = @project.members.collect { |m| m.user }.sort
@rows = @project.members.collect { |m| m.user }
@data = issues_by_assigned_to
@report_title = l(:field_assigned_to)
render :template => "reports/issue_report_details"
when "author"
@field = "author_id"
@rows = @project.members.collect { |m| m.user }.sort
@rows = @project.members.collect { |m| m.user }
@data = issues_by_author
@report_title = l(:field_author)
render :template => "reports/issue_report_details"
@@ -67,11 +67,11 @@ class ReportsController < ApplicationController
render :template => "reports/issue_report_details"
else
@trackers = @project.trackers
@versions = @project.shared_versions.sort
@priorities = IssuePriority.all
@versions = @project.versions.sort
@priorities = Enumeration::get_values('IPRI')
@categories = @project.issue_categories
@assignees = @project.members.collect { |m| m.user }.sort
@authors = @project.members.collect { |m| m.user }.sort
@assignees = @project.members.collect { |m| m.user }
@authors = @project.members.collect { |m| m.user }
@subprojects = @project.descendants.active
issues_by_tracker
issues_by_version
@@ -85,6 +85,42 @@ class ReportsController < ApplicationController
end
end
def delays
@trackers = Tracker.find(:all)
if request.get?
@selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
else
@selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
end
@selected_tracker_ids ||= []
@raw =
ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
FROM issue_histories a, issue_histories b, issues i
WHERE a.status_id =5
AND a.issue_id = b.issue_id
AND a.issue_id = i.id
AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
AND b.id = (
SELECT min( c.id )
FROM issue_histories c
WHERE b.issue_id = c.issue_id )
GROUP BY delay") unless @selected_tracker_ids.empty?
@raw ||=[]
@x_from = 0
@x_to = 0
@y_from = 0
@y_to = 0
@sum_total = 0
@sum_delay = 0
@raw.each do |r|
@x_to = [r['delay'].to_i, @x_to].max
@y_to = [r['total'].to_i, @y_to].max
@sum_total = @sum_total + r['total'].to_i
@sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
end
end
private
# Find project of id params[:id]
def find_project
@@ -130,7 +166,7 @@ private
p.id as priority_id,
count(i.id) as total
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
i.status_id=s.id
and i.priority_id=p.id

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -24,8 +24,6 @@ class InvalidRevisionParam < Exception; end
class RepositoriesController < ApplicationController
menu_item :repository
default_search_scope :changesets
before_filter :find_repository, :except => :edit
before_filter :find_project, :only => :edit
before_filter :authorize
@@ -66,26 +64,31 @@ class RepositoriesController < ApplicationController
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
end
def show
@repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
def show
# check if new revisions have been committed in the repository
@repository.fetch_changesets if Setting.autofetch_changesets?
# root entries
@entries = @repository.entries('', @rev)
# latest changesets
@changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
show_error_not_found unless @entries || @changesets.any?
end
def browse
@entries = @repository.entries(@path, @rev)
if request.xhr?
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
else
(show_error_not_found; return) unless @entries
@changesets = @repository.latest_changesets(@path, @rev)
show_error_not_found and return unless @entries
@properties = @repository.properties(@path, @rev)
render :action => 'show'
render :action => 'browse'
end
end
alias_method :browse, :show
def changes
@entry = @repository.entry(@path, @rev)
(show_error_not_found; return) unless @entry
@changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
show_error_not_found and return unless @entry
@changesets = @repository.changesets_for_path(@path, :limit => Setting.repository_log_display_limit.to_i)
@properties = @repository.properties(@path, @rev)
end
@@ -97,7 +100,7 @@ class RepositoriesController < ApplicationController
@changesets = @repository.changesets.find(:all,
:limit => @changeset_pages.items_per_page,
:offset => @changeset_pages.current.offset,
:include => [:user, :repository])
:include => :user)
respond_to do |format|
format.html { render :layout => false if request.xhr? }
@@ -107,15 +110,15 @@ class RepositoriesController < ApplicationController
def entry
@entry = @repository.entry(@path, @rev)
(show_error_not_found; return) unless @entry
show_error_not_found and return unless @entry
# If the entry is a dir, show the browser
(show; return) if @entry.is_dir?
browse and return if @entry.is_dir?
@content = @repository.cat(@path, @rev)
(show_error_not_found; return) unless @content
if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
# Force the download
show_error_not_found and return unless @content
if 'raw' == params[:format] || @content.is_binary_data?
# Force the download if it's a binary file
send_data @content, :filename => @path.split('/').last
else
# Prevent empty lines when displaying a file with Windows style eol
@@ -125,14 +128,14 @@ class RepositoriesController < ApplicationController
def annotate
@entry = @repository.entry(@path, @rev)
(show_error_not_found; return) unless @entry
show_error_not_found and return unless @entry
@annotate = @repository.scm.annotate(@path, @rev)
(render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty?
render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
end
def revision
@changeset = @repository.find_changeset_by_name(@rev)
@changeset = @repository.changesets.find_by_revision(@rev)
raise ChangesetNotFound unless @changeset
respond_to do |format|
@@ -146,7 +149,7 @@ class RepositoriesController < ApplicationController
def diff
if params[:format] == 'diff'
@diff = @repository.diff(@path, @rev, @rev_to)
(show_error_not_found; return) unless @diff
show_error_not_found and return unless @diff
filename = "changeset_r#{@rev}"
filename << "_r#{@rev_to}" if @rev_to
send_data @diff.join, :filename => "#{filename}.diff",
@@ -196,14 +199,17 @@ private
render_404
end
REV_PARAM_RE = %r{^[a-f0-9]*$}
def find_repository
@project = Project.find(params[:id])
@repository = @project.repository
(render_404; return false) unless @repository
render_404 and return false unless @repository
@path = params[:path].join('/') unless params[:path].nil?
@path ||= ''
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
@rev = params[:rev]
@rev_to = params[:rev_to]
raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
rescue ActiveRecord::RecordNotFound
render_404
rescue InvalidRevisionParam
@@ -232,7 +238,8 @@ private
changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
fields = []
12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
graph = SVG::Graph::Bar.new(
:height => 300,
@@ -261,7 +268,7 @@ private
def graph_commits_per_author(repository)
commits_by_author = repository.changesets.count(:all, :group => :committer)
commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
commits_by_author.sort! {|x, y| x.last <=> y.last}
changes_by_author = repository.changes.count(:all, :group => :committer)
h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}

View File

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

View File

@@ -43,7 +43,7 @@ class SearchController < ApplicationController
begin; offset = params[:offset].to_time if params[:offset]; rescue; end
# quick jump to an issue
if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1)
if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current))
redirect_to :controller => "issues", :action => "show", :id => $1
return
end
@@ -62,8 +62,8 @@ class SearchController < ApplicationController
# extract tokens from the question
# eg. hello "bye bye" => ["hello", "bye bye"]
@tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
# tokens must be at least 2 characters long
@tokens = @tokens.uniq.select {|w| w.length > 1 }
# tokens must be at least 3 character long
@tokens = @tokens.uniq.select {|w| w.length > 2 }
if !@tokens.empty?
# no more than 5 tokens to search for

View File

@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SettingsController < ApplicationController
layout 'admin'
before_filter :require_admin
def index
@@ -26,7 +24,7 @@ class SettingsController < ApplicationController
end
def edit
@notifiables = %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)
settings = (params[:settings] || {}).dup.symbolize_keys
settings.each do |name, value|

View File

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

View File

@@ -46,7 +46,7 @@ class TimelogController < ApplicationController
:klass => Tracker,
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:klass => TimeEntryActivity,
:klass => Enumeration,
:label => :label_activity},
'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
:klass => Issue,
@@ -67,14 +67,7 @@ class TimelogController < ApplicationController
:format => cf.field_format,
:label => cf.name}
end
# Add list and boolean time entry activity custom fields
TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
:format => cf.field_format,
:label => cf.name}
end
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@@ -87,23 +80,15 @@ class TimelogController < ApplicationController
unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
sql_condition = ''
if @project.nil?
sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
elsif @issue.nil?
sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
else
sql_condition = "#{TimeEntry.table_name}.issue_id = #{@issue.id}"
end
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}"
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE"
sql << " (%s) AND" % sql_condition
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries)
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql)
@@ -147,7 +132,7 @@ class TimelogController < ApplicationController
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
end
end
@@ -202,19 +187,16 @@ class TimelogController < ApplicationController
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
:conditions => cond.conditions,
:order => sort_clause)
send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
}
end
end
end
def edit
(render_403; 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)
render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
if request.post? and @time_entry.save
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'details', :project_id => @time_entry.project
@@ -223,8 +205,8 @@ class TimelogController < ApplicationController
end
def destroy
(render_404; return) unless @time_entry
(render_403; return) unless @time_entry.editable_by?(User.current)
render_404 and return unless @time_entry
render_403 and return unless @time_entry.editable_by?(User.current)
@time_entry.destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :back

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -16,16 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TrackersController < ApplicationController
layout 'admin'
before_filter :require_admin
def index
list
render :action => 'list' unless request.xhr?
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
@tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
@@ -56,6 +55,21 @@ class TrackersController < ApplicationController
end
@projects = Project.find(:all)
end
def move
@tracker = Tracker.find(params[:id])
case params[:position]
when 'highest'
@tracker.move_to_top
when 'higher'
@tracker.move_higher
when 'lower'
@tracker.move_lower
when 'lowest'
@tracker.move_to_bottom
end if params[:position]
redirect_to :action => 'list'
end
def destroy
@tracker = Tracker.find(params[:id])

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -16,9 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class UsersController < ApplicationController
layout 'admin'
before_filter :require_admin, :except => :show
before_filter :require_admin
helper :sort
include SortHelper
@@ -26,6 +24,11 @@ class UsersController < ApplicationController
include CustomFieldsHelper
def index
list
render :action => 'list' unless request.xhr?
end
def list
sort_init 'login', 'asc'
sort_update %w(login firstname lastname mail admin created_on last_login_on)
@@ -34,7 +37,7 @@ class UsersController < ApplicationController
unless params[:name].blank?
name = "%#{params[:name].strip.downcase}%"
c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name]
c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
end
@user_count = User.count(:conditions => c.conditions)
@@ -46,29 +49,7 @@ class UsersController < ApplicationController
:limit => @user_pages.items_per_page,
:offset => @user_pages.current.offset
render :layout => !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
render :action => "list", :layout => false if request.xhr?
end
def add
@@ -82,9 +63,7 @@ class UsersController < ApplicationController
if @user.save
Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
flash[:notice] = l(:notice_successful_create)
redirect_to(params[:continue] ? {:controller => 'users', :action => 'add'} :
{:controller => 'users', :action => 'edit', :id => @user})
return
redirect_to :action => 'list'
end
end
@auth_sources = AuthSource.find(:all)
@@ -96,51 +75,30 @@ class UsersController < ApplicationController
@user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
@user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
@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
if @user.update_attributes(params[:user])
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
@auth_sources = AuthSource.find(:all)
@roles = Role.find_all_givable
@projects = Project.active.find(:all, :order => 'lft')
@membership ||= Member.new
rescue ::ActionController::RedirectBackError
redirect_to :controller => 'users', :action => 'edit', :id => @user
@memberships = @user.memberships
end
def edit_membership
@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.save if request.post?
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'
page.visual_effect(:highlight, "member-#{@membership.id}")
}
}
end
redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
end
def destroy_membership
@user = User.find(params[:id])
@membership = Member.find(params[:membership_id])
if request.post? && @membership.deletable?
@membership.destroy
end
respond_to do |format|
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
end
Member.find(params[:membership_id]).destroy if request.post?
redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
end
end

View File

@@ -17,33 +17,17 @@
class VersionsController < ApplicationController
menu_item :roadmap
before_filter :find_version, :except => :close_completed
before_filter :find_project, :only => :close_completed
before_filter :authorize
before_filter :find_project, :authorize
helper :custom_fields
helper :projects
def show
end
def edit
if request.post? && params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
if @version.update_attributes(attributes)
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
if request.post? and @version.update_attributes(params[:version])
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
end
def close_completed
if request.post?
@project.close_completed_versions
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
def destroy
@version.destroy
@@ -61,16 +45,10 @@ class VersionsController < ApplicationController
end
private
def find_version
def find_project
@version = Version.find(params[:id])
@project = @version.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
end

View File

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

View File

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

View File

@@ -18,7 +18,6 @@
require 'diff'
class WikiController < ApplicationController
default_search_scope :wiki_pages
before_filter :find_wiki, :authorize
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
@@ -26,7 +25,6 @@ class WikiController < ApplicationController
helper :attachments
include AttachmentsHelper
helper :watchers
# display a page (in editing mode if it doesn't exist)
def index
@@ -84,7 +82,6 @@ class WikiController < ApplicationController
@content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record
if (@page.new_record? ? @page.save : @content.save)
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
redirect_to :action => 'index', :id => @project, :page => @page.title
end
end
@@ -134,31 +131,9 @@ class WikiController < ApplicationController
render_404 unless @annotate
end
# Removes a wiki page and its history
# Children can be either set as root pages, removed or reassigned to another parent page
# remove a wiki page and its history
def destroy
return render_403 unless editable?
@descendants_count = @page.descendants.size
if @descendants_count > 0
case params[:todo]
when 'nullify'
# Nothing to do
when 'destroy'
# Removes all its descendants
@page.descendants.each(&:destroy)
when 'reassign'
# Reassign children to another parent page
reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
return unless reassign_to
@page.children.each do |child|
child.update_attribute(:parent, reassign_to)
end
else
@reassignable_to = @wiki.pages - @page.self_and_descendants
return
end
end
@page.destroy
redirect_to :action => 'special', :id => @project, :page => 'Page_index'
end
@@ -183,8 +158,7 @@ class WikiController < ApplicationController
return
else
# requested special page doesn't exist, redirect to default page
redirect_to :action => 'index', :id => @project, :page => nil
return
redirect_to :action => 'index', :id => @project, :page => nil and return
end
render :action => "special_#{page_title}"
end

View File

@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WorkflowsController < ApplicationController
layout 'admin'
before_filter :require_admin
def index
@@ -42,42 +40,6 @@ class WorkflowsController < ApplicationController
end
@roles = Role.find(:all, :order => 'builtin, position')
@trackers = Tracker.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
@statuses = IssueStatus.find(:all, :order => 'position')
end
end

View File

@@ -22,12 +22,15 @@ require 'cgi'
module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
include Redmine::I18n
include GravatarHelper::PublicMethods
extend Forwardable
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
def current_role
@current_role ||= User.current.role_for_project(@project)
end
# Return true if user is authorized for controller/action, otherwise false
def authorize_for(controller, action)
User.current.allowed_to?({:controller => controller, :action => action}, @project)
@@ -44,45 +47,16 @@ module ApplicationHelper
link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
end
# Displays a link to user's account page if active
# Display a link to user's account page
def link_to_user(user, options={})
if user.is_a?(User)
name = h(user.name(options[:format]))
if user.active?
link_to name, :controller => 'users', :action => 'show', :id => user
else
name
end
else
h(user.to_s)
end
(user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
end
# Displays a link to +issue+ with its subject.
# Examples:
#
# link_to_issue(issue) # => Defect #6: This is the subject
# link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
# link_to_issue(issue, :subject => false) # => Defect #6
# link_to_issue(issue, :project => true) # => Foo - Defect #6
#
def link_to_issue(issue, options={})
title = nil
subject = nil
if options[:subject] == false
title = truncate(issue.subject, :length => 60)
else
subject = issue.subject
if options[:truncate]
subject = truncate(subject, :length => options[:truncate])
end
end
s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
:class => issue.css_classes,
:title => title
s << ": #{h subject}" if subject
s = "#{h issue.project} - " + s if options[:project]
s
options[:class] ||= ''
options[:class] << ' issue'
options[:class] << ' closed' if issue.closed?
link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
end
# Generates a link to an attachment.
@@ -96,15 +70,6 @@ module ApplicationHelper
link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
end
# Generates a link to a SCM revision
# Options:
# * :text - Link text (default to the formatted revision)
def link_to_revision(revision, project, options={})
text = options.delete(:text) || format_revision(revision)
link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
end
def toggle_link(name, id, options={})
onclick = "Element.toggle('#{id}'); "
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
@@ -124,9 +89,26 @@ module ApplicationHelper
html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
link_to name, {}, html_options
end
def format_date(date)
return nil unless date
# "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
date.strftime(@date_format)
end
def format_time(time, include_date = true)
return nil unless time
time = time.to_time if time.is_a?(String)
zone = User.current.time_zone
local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
@time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
end
def format_activity_title(text)
h(truncate_single_line(text, :length => 100))
h(truncate_single_line(text, 100))
end
def format_activity_day(date)
@@ -134,17 +116,16 @@ module ApplicationHelper
end
def format_activity_description(text)
h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
h(truncate(text.to_s, 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
end
def format_version_name(version)
if version.project == @project
h(version)
else
h("#{version.project} - #{version}")
end
def distance_of_date_in_words(from_date, to_date = 0)
from_date = from_date.to_date if from_date.respond_to?(:to_date)
to_date = to_date.to_date if to_date.respond_to?(:to_date)
distance_in_days = (to_date - from_date).abs
lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
end
def due_date_distance_in_words(date)
if date
l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
@@ -176,24 +157,15 @@ module ApplicationHelper
s
end
# Renders tabs and their content
def render_tabs(tabs)
if tabs.any?
render :partial => 'common/tabs', :locals => {:tabs => tabs}
else
content_tag 'p', l(:label_no_data), :class => "nodata"
end
end
# Renders the project quick-jump box
def render_project_jump_box
# Retrieve them now to avoid a COUNT query
projects = User.current.projects.all
if projects.any?
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
"<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
'<option value="" disabled="disabled">---</option>'
s << project_tree_options_for_select(projects, :selected => @project) do |p|
"<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
'<option disabled="disabled">---</option>'
s << project_tree_options_for_select(projects) do |p|
{ :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
end
s << '</select>'
@@ -247,18 +219,10 @@ module ApplicationHelper
end
s
end
def principals_check_box_tags(name, principals)
s = ''
principals.sort.each do |principal|
s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
end
s
end
# Truncates and returns the string as a single line
def truncate_single_line(string, *args)
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
end
def html_hours(text)
@@ -266,16 +230,25 @@ module ApplicationHelper
end
def authoring(created, author, options={})
l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
link_to(distance_of_time_in_words(Time.now, created),
{:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
:title => format_time(created))
author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
l(options[:label] || :label_added_time_by, author_tag, time_tag)
end
def time_tag(time)
text = distance_of_time_in_words(Time.now, time)
if @project
link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
else
content_tag('acronym', text, :title => format_time(time))
end
def l_or_humanize(s, options={})
k = "#{options[:prefix]}#{s}".to_sym
l_has_string?(k) ? l(k) : s.to_s.humanize
end
def day_name(day)
l(:general_day_names).split(',')[day-1]
end
def month_name(month)
l(:actionview_datehelper_select_month_names).split(',')[month-1]
end
def syntax_highlight(name, content)
@@ -328,13 +301,6 @@ module ApplicationHelper
end
links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
end
def reorder_links(name, url)
link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
end
def breadcrumb(*args)
elements = args.flatten
@@ -342,29 +308,9 @@ module ApplicationHelper
end
def other_formats_links(&block)
concat('<p class="other-formats">' + l(:label_export_to))
concat('<p class="other-formats">' + l(:label_export_to), block.binding)
yield Redmine::Views::OtherFormatsBuilder.new(self)
concat('</p>')
end
def page_header_title
if @project.nil? || @project.new_record?
h(Setting.app_title)
else
b = []
ancestors = (@project.root? ? [] : @project.ancestors.visible)
if ancestors.any?
root = ancestors.shift
b << link_to(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
concat('</p>', block.binding)
end
def html_title(*args)
@@ -373,7 +319,7 @@ module ApplicationHelper
title << @project.name if @project
title += @html_title if @html_title
title << Setting.app_title
title.select {|t| !t.blank? }.join(' - ')
title.compact.join(' - ')
else
@html_title ||= []
@html_title += args
@@ -505,7 +451,7 @@ module ApplicationHelper
# export:some/file -> Force the download of the file
# Forum messages:
# message#1218 -> Link to message with id 1218
text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
link = nil
if esc.nil?
@@ -513,16 +459,17 @@ module ApplicationHelper
if project && (changeset = project.changesets.find_by_revision(oid))
link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
:class => 'changeset',
:title => truncate_single_line(changeset.comments, :length => 100))
:title => truncate_single_line(changeset.comments, 100))
end
elsif sep == '#'
oid = oid.to_i
case prefix
when nil
if issue = Issue.visible.find_by_id(oid, :include => :status)
if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
:class => issue.css_classes,
:title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
:class => (issue.closed? ? 'issue closed' : 'issue'),
:title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
link = content_tag('del', link) if issue.closed?
end
when 'document'
if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
@@ -536,7 +483,7 @@ module ApplicationHelper
end
when 'message'
if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
:controller => 'messages',
:action => 'show',
:board_id => message.board,
@@ -563,7 +510,7 @@ module ApplicationHelper
if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
:class => 'changeset',
:title => truncate_single_line(changeset.comments, :length => 100)
:title => truncate_single_line(changeset.comments, 100)
end
when 'source', 'export'
if project && project.repository
@@ -598,9 +545,46 @@ module ApplicationHelper
gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
end
def error_messages_for(object_name, options = {})
options = options.symbolize_keys
object = instance_variable_get("@#{object_name}")
if object && !object.errors.empty?
# build full_messages here with controller current language
full_messages = []
object.errors.each do |attr, msg|
next if msg.nil?
msg = msg.first if msg.is_a? Array
if attr == "base"
full_messages << l(msg)
else
full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
end
end
# retrieve custom values error messages
if object.errors[:custom_values]
object.custom_values.each do |v|
v.errors.each do |attr, msg|
next if msg.nil?
msg = msg.first if msg.is_a? Array
full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
end
end
end
content_tag("div",
content_tag(
options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
) +
content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
"id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
)
else
""
end
end
def lang_options_for_select(blank=true)
(blank ? [["(auto)", ""]] : []) +
valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
end
def label_tag_for(name, option_tags = nil, options = {})
@@ -628,16 +612,15 @@ module ApplicationHelper
def progress_bar(pcts, options={})
pcts = [pcts, pcts] unless pcts.is_a?(Array)
pcts = pcts.collect(&:round)
pcts[1] = pcts[1] - pcts[0]
pcts << (100 - pcts[1] - pcts[0])
width = options[:width] || '100px;'
legend = options[:legend] || ''
content_tag('table',
content_tag('tr',
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
), :class => 'progress', :style => "width: #{width};") +
content_tag('p', legend, :class => 'pourcent')
end
@@ -668,18 +651,8 @@ module ApplicationHelper
unless @calendar_headers_tags_included
@calendar_headers_tags_included = true
content_for :header_tags do
start_of_week = case Setting.start_of_week.to_i
when 1
'Calendar._FD = 1;' # Monday
when 7
'Calendar._FD = 0;' # Sunday
else
'' # use language
end
javascript_include_tag('calendar/calendar') +
javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
javascript_tag(start_of_week) +
javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
javascript_include_tag('calendar/calendar-setup') +
stylesheet_link_tag('calendar')
end
@@ -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>')
def avatar(user, options = { })
if Setting.gravatar_enabled?
options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
email = nil
if user.respond_to?(:mail)
email = user.mail

View File

@@ -18,15 +18,10 @@
module CustomFieldsHelper
def custom_fields_tabs
tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
{:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
{:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
{:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
{:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
{:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
{:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
{:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
{:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
{:name => 'TimeEntryCustomField', :label => :label_spent_time},
{:name => 'ProjectCustomField', :label => :label_project_plural},
{:name => 'UserCustomField', :label => :label_user_plural}
]
end
@@ -43,7 +38,7 @@ module CustomFieldsHelper
when "text"
text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
when "bool"
hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id)
check_box_tag(field_name, '1', custom_value.true?, :id => field_id) + hidden_field_tag(field_name, '0')
when "list"
blank_option = custom_field.is_required? ?
(custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
@@ -66,26 +61,6 @@ module CustomFieldsHelper
def custom_field_tag_with_label(name, custom_value)
custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
end
def custom_field_tag_for_bulk_edit(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
def show_value(custom_value)
@@ -100,7 +75,7 @@ module CustomFieldsHelper
when "date"
begin; format_date(value.to_date); rescue; value end
when "bool"
l(value == "1" ? :general_text_Yes : :general_text_No)
l_YesNo(value == "1")
else
value
end

View File

@@ -1,34 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module GroupsHelper
# Options for the new membership projects combo-box
def options_for_membership_project_select(user, projects)
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
options << project_tree_options_for_select(projects) do |p|
{:disabled => (user.projects.include?(p))}
end
options
end
def group_settings_tabs
tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
{:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
{:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
]
end
end

View File

@@ -15,6 +15,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'csv'
module IssuesHelper
include ApplicationHelper
@@ -24,29 +26,18 @@ module IssuesHelper
@cached_label_assigned_to ||= l(:field_assigned_to)
@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_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
"<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
end
def render_custom_fields_rows(issue)
return if issue.custom_field_values.empty?
ordered_values = []
half = (issue.custom_field_values.size / 2.0).ceil
half.times do |i|
ordered_values << issue.custom_field_values[i]
ordered_values << issue.custom_field_values[i + half]
end
s = "<tr>\n"
n = 0
ordered_values.compact.each do |value|
s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
n += 1
end
s << "</tr>\n"
# Returns a string of css classes that apply to the given issue
def css_issue_classes(issue)
s = "issue status-#{issue.status.position} priority-#{issue.priority.position}"
s << ' closed' if issue.closed?
s << ' overdue' if issue.overdue?
s
end
@@ -57,7 +48,6 @@ module IssuesHelper
# Project specific queries and global queries
visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
@sidebar_queries = Query.find(:all,
:select => 'id, name',
:order => "name ASC",
:conditions => visible.conditions)
end
@@ -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.old_value) and old_value = u.name if detail.old_value
when 'priority_id'
e = IssuePriority.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.value) and value = e.name if detail.value
e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
when 'category_id'
c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
@@ -129,22 +119,28 @@ module IssuesHelper
case detail.property
when 'attr', 'cf'
if !detail.old_value.blank?
l(:text_journal_changed, :label => label, :old => old_value, :new => value)
label + " " + l(:text_journal_changed, old_value, value)
else
l(:text_journal_set_to, :label => label, :value => value)
label + " " + l(:text_journal_set_to, value)
end
when 'attachment'
l(:text_journal_added, :label => label, :value => value)
"#{label} #{value} #{l(:label_added)}"
end
else
l(:text_journal_deleted, :label => label, :old => old_value)
case detail.property
when 'attr', 'cf'
label + " " + l(:text_journal_deleted) + " (#{old_value})"
when 'attachment'
"#{label} #{old_value} #{l(:label_deleted)}"
end
end
end
def issues_to_csv(issues, project = nil)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
decimal_separator = l(:general_csv_decimal_separator)
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [ "#",
l(:field_status),
@@ -194,6 +190,7 @@ module IssuesHelper
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
export.rewind
export
end
end

View File

@@ -30,9 +30,7 @@ module JournalsHelper
end
content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
content << textilizable(journal, :notes)
css_classes = "wiki"
css_classes << " editable" if editable
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => css_classes)
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'wiki editable' : 'wiki'))
end
def link_to_in_place_notes_editor(text, field_id, url, options={})

View File

@@ -19,7 +19,7 @@ module MessagesHelper
def link_to_message(message)
return '' unless message
link_to h(truncate(message.subject, :length => 60)), :controller => 'messages',
link_to h(truncate(message.subject, 60)), :controller => 'messages',
:action => 'show',
:board_id => message.board_id,
:id => message.root,

View File

@@ -18,7 +18,7 @@
module ProjectsHelper
def link_to_version(version, options = {})
return '' unless version && version.is_a?(Version)
link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
end
def project_settings_tabs
@@ -29,23 +29,13 @@ module ProjectsHelper
{:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
{:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
{:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
{:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
{:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
{:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
]
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end
def parent_project_select_tag(project)
selected = project.parent
# retrieve the requested parent project
parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
if parent_id
selected = (parent_id.blank? ? nil : Project.find(parent_id))
end
options = ''
options << "<option value=''></option>" if project.allowed_parents.include?(nil)
options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
options = '<option></option>' + project_tree_options_for_select(project.possible_parents, :selected => project.parent)
content_tag('select', options, :name => 'project[parent_id]')
end
@@ -78,27 +68,4 @@ module ProjectsHelper
end
s
end
# Returns a set of options for a select field, grouped by project.
def version_options_for_select(versions, selected=nil)
grouped = Hash.new {|h,k| h[k] = []}
versions.each do |version|
grouped[version.project.name] << [version.name, version.id]
end
# Add in the selected
if selected && !versions.include?(selected)
grouped[selected.project.name] << [selected.name, selected.id]
end
if grouped.keys.size > 1
grouped_options_for_select(grouped, selected && selected.id)
else
options_for_select((grouped.values.first || []), selected && selected.id)
end
end
def format_version_sharing(sharing)
sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
l("label_version_sharing_#{sharing}")
end
end

View File

@@ -28,37 +28,28 @@ module QueriesHelper
end
def column_content(column, issue)
value = column.value(issue)
case value.class.name
when 'String'
if column.name == :subject
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
else
h(value)
end
when 'Time'
format_time(value)
when 'Date'
format_date(value)
when 'Fixnum', 'Float'
if column.name == :done_ratio
progress_bar(value, :width => '80px')
else
value.to_s
end
when 'User'
link_to_user value
when 'Project'
link_to(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)
if column.is_a?(QueryCustomFieldColumn)
cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
show_value(cv)
else
h(value)
value = issue.send(column.name)
if value.is_a?(Date)
format_date(value)
elsif value.is_a?(Time)
format_time(value)
else
case column.name
when :subject
h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
when :done_ratio
progress_bar(value, :width => '80px')
when :fixed_version
link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
else
h(value)
end
end
end
end
end

View File

@@ -121,7 +121,7 @@ module RepositoriesHelper
def repository_field_tags(form, repository)
method = repository.class.name.demodulize.underscore + "_field_tags"
send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method) && method != 'repository_field_tags'
send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
end
def scm_select_tag(repository)
@@ -147,12 +147,13 @@ module RepositoriesHelper
def subversion_field_tags(form, repository)
content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
'<br />(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
'<br />(http://, https://, svn://, file:///)') +
content_tag('p', form.text_field(:login, :size => 30)) +
content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
:value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
:onfocus => "this.value=''; this.name='repository[password]';",
:onchange => "this.name='repository[password]';"))
:onchange => "this.name='repository[password]';")) +
content_tag('p', form.check_box(:cache))
end
def darcs_field_tags(form, repository)

View File

@@ -27,9 +27,8 @@ module SearchHelper
result << '...'
break
end
words = words.mb_chars
if i.even?
result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
result << h(words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words)
else
t = (tokens.index(words.downcase) || 0) % 4
result << content_tag('span', h(words), :class => "highlight token-#{t}")

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -22,53 +22,9 @@ module SettingsHelper
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
{:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
{:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
{:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
{:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)},
{:name => 'mail_handler', :partial => 'settings/mail_handler', :label => l(:label_incoming_emails)},
{:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
]
end
def setting_select(setting, choices, options={})
if blank_text = options.delete(:blank)
choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
end
setting_label(setting, options) +
select_tag("settings[#{setting}]", options_for_select(choices, Setting.send(setting).to_s), options)
end
def setting_multiselect(setting, choices, options={})
setting_values = Setting.send(setting)
setting_values = [] unless setting_values.is_a?(Array)
setting_label(setting, options) +
hidden_field_tag("settings[#{setting}][]", '') +
choices.collect do |choice|
text, value = (choice.is_a?(Array) ? choice : [choice, choice])
content_tag('label',
check_box_tag("settings[#{setting}][]", value, Setting.send(setting).include?(value)) + text.to_s,
:class => 'block'
)
end.join
end
def setting_text_field(setting, options={})
setting_label(setting, options) +
text_field_tag("settings[#{setting}]", Setting.send(setting), options)
end
def setting_text_area(setting, options={})
setting_label(setting, options) +
text_area_tag("settings[#{setting}]", Setting.send(setting), options)
end
def setting_check_box(setting, options={})
setting_label(setting, options) +
hidden_field_tag("settings[#{setting}]", 0) +
check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options)
end
def setting_label(setting, options={})
label = options.delete(:label)
label != false ? content_tag("label", l(label || "setting_#{setting}")) : ''
end
end

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ module UsersHelper
end
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?
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},
{:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
]
if Group.all.any?
tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
end
tabs
end
end

View File

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

View File

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

View File

@@ -33,7 +33,7 @@ class Attachment < ActiveRecord::Base
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"}
acts_as_activity_provider :type => 'documents',
:permission => :view_documents,
@@ -46,9 +46,7 @@ class Attachment < ActiveRecord::Base
@@storage_path = "#{RAILS_ROOT}/files"
def validate
if self.filesize > Setting.attachment_max_size.to_i.kilobytes
errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
end
errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
end
def file=(incoming_file)
@@ -58,9 +56,6 @@ class Attachment < ActiveRecord::Base
self.filename = sanitize_filename(@temp_file.original_filename)
self.disk_filename = Attachment.disk_filename(filename)
self.content_type = @temp_file.content_type.to_s.chomp
if content_type.blank?
self.content_type = Redmine::MimeType.of(filename)
end
self.filesize = @temp_file.size
end
end
@@ -70,20 +65,14 @@ class Attachment < ActiveRecord::Base
nil
end
# Copies the temporary file to its final location
# and computes its MD5 hash
# Copy temp file to its final location
def before_save
if @temp_file && (@temp_file.size > 0)
logger.debug("saving '#{self.diskfile}'")
md5 = Digest::MD5.new
File.open(diskfile, "wb") do |f|
buffer = ""
while (buffer = @temp_file.read(8192))
f.write(buffer)
md5.update(buffer)
end
f.write(@temp_file.read)
end
self.digest = md5.hexdigest
self.digest = self.class.digest(diskfile)
end
# Don't save the content type if it's longer than the authorized length
if self.content_type && self.content_type.length > 255
@@ -129,11 +118,6 @@ class Attachment < ActiveRecord::Base
self.filename =~ /\.(patch|diff)$/i
end
# Returns true if the file is readable
def readable?
File.readable?(diskfile)
end
private
def sanitize_filename(value)
# get only the filename, not the whole path
@@ -157,4 +141,11 @@ private
end
df
end
# Returns the MD5 digest of the file at given path
def self.digest(filename)
File.open(filename, 'rb') do |f|
Digest::MD5.hexdigest(f.read)
end
end
end

View File

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

View File

@@ -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))},
:description => :long_comments,
: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',
:include => {:repository => :project},
@@ -35,15 +35,12 @@ class Changeset < ActiveRecord::Base
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
:author_key => :user_id,
:find_options => {:include => [:user, {:repository => :project}]}
:find_options => {:include => {:repository => :project}}
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
validates_uniqueness_of :revision, :scope => :repository_id
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
named_scope :visible, lambda {|*args| { :include => {:repository => :project},
:conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
def revision=(r)
write_attribute :revision, (r.nil? ? nil : r.to_s)
end
@@ -92,14 +89,14 @@ class Changeset < ActiveRecord::Base
if ref_keywords.delete('*')
# find any issue ID in the comments
target_issue_ids = []
comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
referenced_issues += find_referenced_issues_by_id(target_issue_ids)
comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
end
comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
action = match[0]
target_issue_ids = match[1].scan(/\d+/)
target_issues = find_referenced_issues_by_id(target_issue_ids)
target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
if fix_status && fix_keywords.include?(action.downcase)
# update status of issues
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
@@ -112,12 +109,11 @@ class Changeset < ActiveRecord::Base
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
csettext = "commit:\"#{self.scmid}\""
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.done_ratio = done_ratio if done_ratio
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
{ :changeset => self, :issue => issue })
issue.save
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
end
referenced_issues += target_issues
@@ -151,14 +147,6 @@ class Changeset < ActiveRecord::Base
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)
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
comments =~ /\A(.+?)\r?\n(.*)$/m
@short_comments = $1 || comments

View File

@@ -48,14 +48,14 @@ class CustomField < ActiveRecord::Base
def validate
if self.field_format == "list"
errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
end
# validate default value
v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
v.custom_field.is_required = false
errors.add(:default_value, :invalid) unless v.valid?
errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
end
# Makes possible_values accept a multiline string
@@ -67,25 +67,6 @@ class CustomField < ActiveRecord::Base
end
end
def cast_value(value)
casted = nil
unless value.blank?
case field_format
when 'string', 'text', 'list'
casted = value
when 'date'
casted = begin; value.to_date; rescue; nil end
when 'bool'
casted = (value == '1' ? true : false)
when 'int'
casted = value.to_i
when 'float'
casted = value.to_f
end
end
casted
end
# Returns a ORDER BY clause that can used to sort customized
# objects by their value of the custom field.
# Returns false, if the custom field can not be used for sorting.

View File

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

View File

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

View File

@@ -1,34 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class DocumentCategory < Enumeration
has_many :documents, :foreign_key => 'category_id'
OptionName = :enumeration_doc_categories
def option_name
OptionName
end
def objects_count
documents.count
end
def transfer_relations(to)
documents.update_all("category_id = #{to.id}")
end
end

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -20,19 +20,4 @@ class EnabledModule < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name, :scope => :project_id
after_create :module_enabled
private
# after_create callback used to do things when a module is enabled
def module_enabled
case name
when 'wiki'
# Create a wiki with a default start page
if project && project.wiki.nil?
Wiki.create(:project => project, :start_page => 'Wiki')
end
end
end
end

View File

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

View File

@@ -1,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

View File

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

View File

@@ -22,7 +22,7 @@ class Issue < ActiveRecord::Base
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
has_many :journals, :as => :journalized, :dependent => :destroy
@@ -39,14 +39,12 @@ class Issue < ActiveRecord::Base
:include => [:project, :journals],
# sort by id so that limited eager loading doesn't break with postgresql
:order_column => "#{table_name}.id"
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
:type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
:author_key => :author_id
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
validates_presence_of :subject, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255
@@ -56,11 +54,6 @@ class Issue < ActiveRecord::Base
named_scope :visible, lambda {|*args| { :include => :project,
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
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
def visible?(usr=nil)
(usr || User.current).allowed_to?(:view_issues, self.project)
@@ -70,7 +63,7 @@ class Issue < ActiveRecord::Base
if new_record?
# set default values for new records only
self.status ||= IssueStatus.default
self.priority ||= IssuePriority.default
self.priority ||= Enumeration.default('IPRI')
end
end
@@ -81,9 +74,8 @@ class Issue < ActiveRecord::Base
def copy_from(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.status = issue.status
self
end
@@ -103,10 +95,7 @@ class Issue < ActiveRecord::Base
# reassign to the category with same name if any
new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
issue.category = new_category
# Keep the fixed_version if it's still valid in the new_project
unless new_project.shared_versions.include?(issue.fixed_version)
issue.fixed_version = nil
end
issue.fixed_version = nil
issue.project = new_project
end
if new_tracker
@@ -114,15 +103,7 @@ class Issue < ActiveRecord::Base
end
if options[:copy]
issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
issue.status = if options[:attributes] && options[:attributes][:status_id]
IssueStatus.find_by_id(options[:attributes][:status_id])
else
self.status
end
end
# Allow bulk setting of attributes on the issue
if options[:attributes]
issue.attributes = options[:attributes]
issue.status = self.status
end
if issue.save
unless options[:copy]
@@ -141,75 +122,29 @@ class Issue < ActiveRecord::Base
self.priority = nil
write_attribute(:priority_id, pid)
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
alias_method_chain :attributes=, :tracker_first
def estimated_hours=(h)
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
end
def done_ratio
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
status.default_done_ratio
else
read_attribute(:done_ratio)
end
end
def self.use_status_for_done_ratio?
Setting.issue_done_ratio == 'issue_status'
end
def self.use_field_for_done_ratio?
Setting.issue_done_ratio == 'issue_field'
end
def validate
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
errors.add :due_date, :not_a_date
errors.add :due_date, :activerecord_error_not_a_date
end
if self.due_date and self.start_date and self.due_date < self.start_date
errors.add :due_date, :greater_than_start_date
errors.add :due_date, :activerecord_error_greater_than_start_date
end
if start_date && soonest_start && start_date < soonest_start
errors.add :start_date, :invalid
end
if fixed_version
if !assignable_versions.include?(fixed_version)
errors.add :fixed_version_id, :inclusion
elsif reopened? && fixed_version.closed?
errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
end
end
# Checks that the issue can not be added/moved to a disabled tracker
if project && (tracker_id_changed? || project_id_changed?)
unless project.trackers.include?(tracker)
errors.add :tracker_id, :inclusion
end
errors.add :start_date, :activerecord_error_invalid
end
end
def validate_on_create
errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
end
def before_create
# default assignment based on category
if assigned_to.nil? && category && category.assigned_to
@@ -217,12 +152,28 @@ class Issue < ActiveRecord::Base
end
end
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios
# even if the user turns off the setting later
def update_done_ratio_from_issue_status
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
self.done_ratio = status.default_done_ratio
def before_save
if @current_journal
# attributes changes
(Issue.column_names - %w(id description)).each {|c|
@current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c,
:old_value => @issue_before_change.send(c),
:value => send(c)) unless send(c)==@issue_before_change.send(c)
}
# custom fields changes
custom_values.each {|c|
next if (@custom_values_before_change[c.custom_field_id]==c.value ||
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
@current_journal.details << JournalDetail.new(:property => 'cf',
:prop_key => c.custom_field_id,
:old_value => @custom_values_before_change[c.custom_field_id],
:value => c.value)
}
@current_journal.save
end
# Save the issue even if the journal is not saved (because empty)
true
end
def after_save
@@ -262,18 +213,6 @@ class Issue < ActiveRecord::Base
self.status.is_closed?
end
# Return true if the issue is being reopened
def reopened?
if !new_record? && status_id_changed?
status_was = IssueStatus.find_by_id(status_id_was)
status_new = IssueStatus.find_by_id(status_id)
if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
return true
end
end
false
end
# Returns true if the issue is overdue
def overdue?
!due_date.nil? && (due_date < Date.today) && !status.is_closed?
@@ -284,41 +223,22 @@ class Issue < ActiveRecord::Base
project.assignable_users
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
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 = statuses.uniq.sort
blocked? ? statuses.reject {|s| s.is_closed?} : statuses
statuses.uniq.sort
end
# Returns the mail adresses of users that should be notified
# Returns the mail adresses of users that should be notified for the issue
def recipients
notified = project.notified_users
recipients = project.recipients
# Author and assignee are always notified unless they have been locked
notified << author if author && author.active?
notified << assigned_to if assigned_to && assigned_to.active?
notified.uniq!
# Remove users that can not view the issue
notified.reject! {|user| !visible?(user)}
notified.collect(&:mail)
recipients << author.mail if author && author.active?
recipients << assigned_to.mail if assigned_to && assigned_to.active?
recipients.compact.uniq
end
# Returns the total number of hours spent on this issue.
#
# Example:
# spent_hours => 0
# spent_hours => 50
def spent_hours
@spent_hours ||= time_entries.sum(:hours) || 0
end
@@ -347,11 +267,6 @@ class Issue < ActiveRecord::Base
due_date || (fixed_version ? fixed_version.effective_date : nil)
end
# Returns the time scheduled for this issue.
#
# Example:
# Start Date: 2/26/09, End Date: 3/04/09
# duration => 6
def duration
(start_date && due_date) ? due_date - start_date : 0
end
@@ -364,52 +279,8 @@ class Issue < ActiveRecord::Base
"#{tracker} ##{id}: #{subject}"
end
# Returns a string of css classes that apply to the issue
def css_classes
s = "issue status-#{status.position} priority-#{priority.position}"
s << ' closed' if closed?
s << ' overdue' if overdue?
s << ' created-by-me' if User.current.logged? && author_id == User.current.id
s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
s
end
# 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
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
def attachment_removed(obj)
journal = init_journal(User.current)
@@ -418,28 +289,4 @@ class Issue < ActiveRecord::Base
:old_value => obj.filename)
journal.save
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

View File

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

View File

@@ -1,34 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssuePriority < Enumeration
has_many :issues, :foreign_key => 'priority_id'
OptionName = :enumeration_issue_priorities
def option_name
OptionName
end
def objects_count
issues.count
end
def transfer_relations(to)
issues.update_all("priority_id = #{to.id}")
end
end

View File

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

View File

@@ -23,13 +23,11 @@ class IssueRelation < ActiveRecord::Base
TYPE_DUPLICATES = "duplicates"
TYPE_BLOCKS = "blocks"
TYPE_PRECEDES = "precedes"
TYPE_FOLLOWS = "follows"
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 5 }
}.freeze
validates_presence_of :issue_from, :issue_to, :relation_type
@@ -41,9 +39,9 @@ class IssueRelation < ActiveRecord::Base
def validate
if issue_from && issue_to
errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from
errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id
errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from
end
end
@@ -56,8 +54,6 @@ class IssueRelation < ActiveRecord::Base
end
def before_save
reverse_if_needed
if TYPE_PRECEDES == relation_type
self.delay ||= 0
else
@@ -82,15 +78,4 @@ class IssueRelation < ActiveRecord::Base
def <=>(relation)
TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
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

View File

@@ -24,7 +24,6 @@ class IssueStatus < ActiveRecord::Base
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
def after_save
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
@@ -34,49 +33,27 @@ class IssueStatus < ActiveRecord::Base
def self.default
find(:first, :conditions =>["is_default=?", true])
end
# Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
def self.update_issue_done_ratios
if Issue.use_status_for_done_ratio?
IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
Issue.update_all(["done_ratio = ?", status.default_done_ratio],
["status_id = ?", status.id])
end
end
return Issue.use_status_for_done_ratio?
end
# Returns an array of all statuses the given role can switch to
# Uses association cache when called more than one time
def new_statuses_allowed_to(roles, tracker)
if roles && tracker
role_ids = roles.collect(&:id)
new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
else
[]
end
def new_statuses_allowed_to(role, tracker)
new_statuses = workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker
new_statuses ? new_statuses.compact.sort{|x, y| x.position <=> y.position } : []
end
# Same thing as above but uses a database query
# More efficient than the previous method if called just once
def find_new_statuses_allowed_to(roles, tracker)
if roles && tracker
workflows.find(:all,
:include => :new_status,
:conditions => { :role_id => roles.collect(&:id),
:tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
else
[]
end
def find_new_statuses_allowed_to(role, tracker)
new_statuses = workflows.find(:all,
:include => :new_status,
:conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact if role && tracker
new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : []
end
def new_status_allowed_to?(status, roles, tracker)
if status && roles && tracker
!workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
else
def new_status_allowed_to?(status, role, tracker)
status && role && tracker ?
!workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => role.id, :tracker_id => tracker.id}).nil? :
false
end
end
def <=>(status)

View File

@@ -38,7 +38,7 @@ class Journal < ActiveRecord::Base
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
def save(*args)
def save
# Do not save an empty journal
(details.empty? && notes.blank?) ? false : super
end

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

View File

@@ -34,46 +34,19 @@ class MailHandler < ActionMailer::Base
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
# Status overridable by default
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
@@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
super email
end
# Processes incoming emails
# Returns the created object (eg. an issue, a message) or false
def receive(email)
@email = email
sender_email = email.from.to_a.first.to_s.strip
# Ignore emails received from the application emission address to avoid hell cycles
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
@user = User.active.find_by_mail(email.from.first.to_s.strip)
unless @user
# Unknown user => the email is ignored
# TODO: ability to create the user's account
logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
return false
end
@user = User.find_by_mail(sender_email)
if @user && !@user.active?
logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
return false
end
if @user.nil?
# Email was submitted by an unknown user
case @@handler_options[:unknown_user]
when 'accept'
@user = User.anonymous
when 'create'
@user = MailHandler.create_user_from_email(email)
if @user
logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
Mailer.deliver_account_information(@user, @user.password)
else
logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
return false
end
else
# Default behaviour, emails from unknown users are ignored
logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
return false
end
end
User.current = @user
dispatch
end
@@ -81,15 +54,15 @@ class MailHandler < ActionMailer::Base
private
MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]}
def dispatch
headers = [email.in_reply_to, email.references].flatten.compact
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
klass, object_id = $1, $2.to_i
method_name = "receive_#{klass}_reply"
if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
if self.class.private_instance_methods.include?(method_name)
send method_name, object_id
else
# ignoring it
@@ -118,23 +91,18 @@ class MailHandler < ActionMailer::Base
project = target_project
tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
priority = (get_keyword(:priority) && 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)))
# check permission
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
end
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
# check workflow
if status && issue.new_statuses_allowed_to(user).include?(status)
issue.status = status
end
issue.subject = email.subject.chomp
if issue.subject.blank?
issue.subject = '(no subject)'
end
issue.subject = email.subject.chomp.toutf8
issue.description = plain_text_body
# custom fields
issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
if value = get_keyword(c.name, :override => true)
@@ -142,12 +110,13 @@ class MailHandler < ActionMailer::Base
end
h
end
issue.description = cleaned_up_text_body
# add To and Cc as watchers before saving so the watchers can reply to Redmine
add_watchers(issue)
issue.save!
add_attachments(issue)
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
# add To and Cc as watchers
add_watchers(issue)
# send notification after adding watchers so that they can reply to Redmine
Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
issue
end
@@ -167,13 +136,11 @@ class MailHandler < ActionMailer::Base
issue = Issue.find_by_id(issue_id)
return unless issue
# check permission
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
end
raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
# add the note
journal = issue.init_journal(user, cleaned_up_text_body)
journal = issue.init_journal(user, plain_text_body)
add_attachments(issue)
# check workflow
if status && issue.new_statuses_allowed_to(user).include?(status)
@@ -181,6 +148,7 @@ class MailHandler < ActionMailer::Base
end
issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
journal
end
@@ -197,21 +165,16 @@ class MailHandler < ActionMailer::Base
message = Message.find_by_id(message_id)
if message
message = message.root
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
end
if !message.locked?
if user.allowed_to?(:add_messages, message.project) && !message.locked?
reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
:content => cleaned_up_text_body)
:content => plain_text_body)
reply.author = user
reply.board = message.board
message.children << reply
add_attachments(reply)
reply
else
logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
raise UnauthorizedAction
end
end
end
@@ -245,7 +208,7 @@ class MailHandler < ActionMailer::Base
@keywords[attr]
else
@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
elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr]
@@ -272,45 +235,5 @@ class MailHandler < ActionMailer::Base
@plain_text_body = plain_text_part.body.to_s
end
@plain_text_body.strip!
@plain_text_body
end
def cleaned_up_text_body
cleanup_body(plain_text_body)
end
def self.full_sanitizer
@full_sanitizer ||= HTML::FullSanitizer.new
end
# Creates a user account for the +email+ sender
def self.create_user_from_email(email)
addr = email.from_addrs.to_a.first
if addr && !addr.spec.blank?
user = User.new
user.mail = addr.spec
names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
user.firstname = names.shift
user.lastname = names.join(' ')
user.lastname = '-' if user.lastname.blank?
user.login = user.mail
user.password = ActiveSupport::SecureRandom.hex(5)
user.language = Setting.default_language
user.save ? user : nil
end
end
private
# Removes the email body of text after the truncation configurations.
def cleanup_body(body)
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
unless delimiters.empty?
regex = Regexp.new("^(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
body = body.gsub(regex, '')
end
body.strip
end
end

View File

@@ -16,25 +16,12 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Mailer < ActionMailer::Base
layout 'mailer'
helper :application
helper :issues
helper :custom_fields
include ActionController::UrlWriter
include Redmine::I18n
def self.default_url_options
h = Setting.host_name
h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
{ :host => h, :protocol => Setting.protocol }
end
# Builds a tmail object used to email recipients of the added issue.
#
# Example:
# issue_add(issue) => tmail object
# Mailer.deliver_issue_add(issue) => sends an email to issue recipients
def issue_add(issue)
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
@@ -46,16 +33,10 @@ class Mailer < ActionMailer::Base
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
body :issue => issue,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
render_multipart('issue_add', body)
end
# Builds a tmail object used to email recipients of the edited issue.
#
# Example:
# issue_edit(journal) => tmail object
# Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
def issue_edit(journal)
issue = journal.journalized.reload
issue = journal.journalized
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
@@ -73,8 +54,6 @@ class Mailer < ActionMailer::Base
body :issue => issue,
:journal => journal,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
render_multipart('issue_edit', body)
end
def reminder(user, issues, days)
@@ -84,28 +63,16 @@ class Mailer < ActionMailer::Base
body :issues => issues,
:days => days,
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
render_multipart('reminder', body)
end
# Builds a tmail object used to email users belonging to the added document's project.
#
# Example:
# document_added(document) => tmail object
# Mailer.deliver_document_added(document) => sends an email to the document's project recipients
def document_added(document)
redmine_headers 'Project' => document.project.identifier
recipients document.recipients
recipients document.project.recipients
subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
body :document => document,
:document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
render_multipart('document_added', body)
end
# Builds a tmail object used to email recipients of a project when an attachements are added.
#
# Example:
# attachments_added(attachments) => tmail object
# Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
def attachments_added(attachments)
container = attachments.first.container
added_to = ''
@@ -114,97 +81,41 @@ class Mailer < ActionMailer::Base
when 'Project'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
added_to = "#{l(:label_project)}: #{container}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
when 'Version'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to = "#{l(:label_version)}: #{container.name}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
when 'Document'
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
added_to = "#{l(:label_document)}: #{container.title}"
recipients container.recipients
end
redmine_headers 'Project' => container.project.identifier
recipients container.project.recipients
subject "[#{container.project.name}] #{l(:label_attachment_new)}"
body :attachments => attachments,
:added_to => added_to,
:added_to_url => added_to_url
render_multipart('attachments_added', body)
end
# Builds a tmail object used to email recipients of a news' project when a news item is added.
#
# Example:
# news_added(news) => tmail object
# Mailer.deliver_news_added(news) => sends an email to the news' project recipients
def news_added(news)
redmine_headers 'Project' => news.project.identifier
message_id news
recipients news.recipients
recipients news.project.recipients
subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
body :news => news,
:news_url => url_for(:controller => 'news', :action => 'show', :id => news)
render_multipart('news_added', body)
end
# Builds a tmail object used to email the recipients of the specified message that was posted.
#
# Example:
# message_posted(message) => tmail object
# Mailer.deliver_message_posted(message) => sends an email to the recipients
def message_posted(message)
def message_posted(message, recipients)
redmine_headers 'Project' => message.project.identifier,
'Topic-Id' => (message.parent_id || message.id)
message_id message
references message.parent unless message.parent.nil?
recipients(message.recipients)
cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
recipients(recipients)
subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
body :message => message,
:message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
render_multipart('message_posted', body)
end
# Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
#
# Example:
# wiki_content_added(wiki_content) => tmail object
# Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
def wiki_content_added(wiki_content)
redmine_headers 'Project' => wiki_content.project.identifier,
'Wiki-Page-Id' => wiki_content.page.id
message_id wiki_content
recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients - recipients)
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :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)
set_language_if_valid user.language
recipients user.mail
@@ -212,35 +123,14 @@ class Mailer < ActionMailer::Base
body :user => user,
:password => password,
:login_url => url_for(:controller => 'account', :action => 'login')
render_multipart('account_information', body)
end
# Builds a tmail object used to email all active administrators of an account activation request.
#
# Example:
# account_activation_request(user) => tmail object
# Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
def account_activation_request(user)
# Send the email to all active administrators
recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
subject l(:mail_subject_account_activation_request, Setting.app_title)
body :user => user,
:url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
render_multipart('account_activation_request', body)
end
# Builds a tmail object used to email the specified user that their account was activated by an administrator.
#
# Example:
# account_activated(user) => tmail object
# Mailer.deliver_account_activated(user) => sends an email to the registered user
def account_activated(user)
set_language_if_valid user.language
recipients user.mail
subject l(:mail_subject_register, Setting.app_title)
body :user => user,
:login_url => url_for(:controller => 'account', :action => 'login')
render_multipart('account_activated', body)
end
def lost_password(token)
@@ -249,7 +139,6 @@ class Mailer < ActionMailer::Base
subject l(:mail_subject_lost_password, Setting.app_title)
body :token => token,
:url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
render_multipart('lost_password', body)
end
def register(token)
@@ -258,7 +147,6 @@ class Mailer < ActionMailer::Base
subject l(:mail_subject_register, Setting.app_title)
body :token => token,
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
render_multipart('register', body)
end
def test(user)
@@ -266,13 +154,11 @@ class Mailer < ActionMailer::Base
recipients user.mail
subject 'Redmine test'
body :url => url_for(:controller => 'welcome')
render_multipart('test', body)
end
# Overrides default deliver! method to prevent from sending an email
# with no recipient, cc or bcc
def deliver!(mail = @mail)
set_language_if_valid @initial_language
return false if (recipients.nil? || recipients.empty?) &&
(cc.nil? || cc.empty?) &&
(bcc.nil? || bcc.empty?)
@@ -314,16 +200,19 @@ class Mailer < ActionMailer::Base
private
def initialize_defaults(method_name)
super
@initial_language = current_language
set_language_if_valid Setting.default_language
from Setting.mail_from
# URL options
h = Setting.host_name
h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
default_url_options[:host] = h
default_url_options[:protocol] = Setting.protocol
# Common headers
headers 'X-Mailer' => 'Redmine',
'X-Redmine-Host' => Setting.host_name,
'X-Redmine-Site' => Setting.app_title,
'Precedence' => 'bulk',
'Auto-Submitted' => 'auto-generated'
'X-Redmine-Site' => Setting.app_title
end
# Appends a Redmine header field (name is prepended with 'X-Redmine-')
@@ -349,22 +238,26 @@ class Mailer < ActionMailer::Base
super
end
# Rails 2.3 has problems rendering implicit multipart messages with
# layouts so this method will wrap an multipart messages with
# explicit parts.
#
# https://rails.lighthouseapp.com/projects/8994/tickets/2338-actionmailer-mailer-views-and-content-type
# https://rails.lighthouseapp.com/projects/8994/tickets/1799-actionmailer-doesnt-set-template_format-when-rendering-layouts
def render_multipart(method_name, body)
# Renders a message with the corresponding layout
def render_message(method_name, body)
layout = method_name.to_s.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
body[:content_for_layout] = render(:file => method_name, :body => body)
ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
end
# for the case of plain text only
def body(*params)
value = super(*params)
if Setting.plain_text_mail?
content_type "text/plain"
body render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb')
else
content_type "multipart/alternative"
part :content_type => "text/plain", :body => render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb')
part :content_type => "text/html", :body => render_message("#{method_name}.text.html.rhtml", body)
templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
unless String === @body or templates.empty?
template = File.basename(templates.first)
@body[:content_for_layout] = render(:file => template, :body => @body)
@body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
return @body
end
end
return value
end
# Makes partial rendering work with Rails 1.2 (retro-compatibility)
@@ -376,8 +269,7 @@ class Mailer < ActionMailer::Base
def self.message_id_for(object)
# id + timestamp should reduce the odds of a collision
# as far as we don't send multiple emails for the same object
timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{object.created_on.strftime("%Y%m%d%H%M%S")}"
host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
host = "#{::Socket.gethostname}.redmine" if host.empty?
"<#{hash}@#{host}>"

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -17,73 +17,26 @@
class Member < ActiveRecord::Base
belongs_to :user
belongs_to :principal, :foreign_key => 'user_id'
has_many :member_roles, :dependent => :destroy
has_many :roles, :through => :member_roles
belongs_to :role
belongs_to :project
validates_presence_of :principal, :project
validates_presence_of :role, :user, :project
validates_uniqueness_of :user_id, :scope => :project_id
after_destroy :unwatch_from_permission_change
def validate
errors.add :role_id, :activerecord_error_invalid if role && !role.member?
end
def name
self.user.name
end
alias :base_role_ids= :role_ids=
def role_ids=(arg)
ids = (arg || []).collect(&:to_i) - [0]
# Keep inherited roles
ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
new_role_ids = ids - role_ids
# Add new roles
new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
# Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
member_roles_to_destroy = member_roles.select {|mr| !ids.include?(mr.role_id)}
if member_roles_to_destroy.any?
member_roles_to_destroy.each(&:destroy)
unwatch_from_permission_change
end
end
def <=>(member)
a, b = roles.sort.first, member.roles.sort.first
a == b ? (principal <=> member.principal) : (a <=> b)
end
def deletable?
member_roles.detect {|mr| mr.inherited_from}.nil?
end
def include?(user)
if principal.is_a?(Group)
!user.nil? && user.groups.include?(principal)
else
self.user == user
end
role == member.role ? (user <=> member.user) : (role <=> member.role)
end
def before_destroy
if user
# remove category based auto assignments for this member
IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
end
end
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
# remove category based auto assignments for this member
IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
end
end

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

View File

@@ -37,41 +37,32 @@ class Message < ActiveRecord::Base
acts_as_watchable
attr_protected :locked, :sticky
validates_presence_of :board, :subject, :content
validates_presence_of :subject, :content
validates_length_of :subject, :maximum => 255
after_create :add_author_as_watcher
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_messages, project)
end
def validate_on_create
# Can not reply to a locked topic
errors.add_to_base 'Topic is locked' if root.locked? && self != root
end
def after_create
board.update_attribute(:last_message_id, self.id)
board.increment! :messages_count
if parent
parent.reload.update_attribute(:last_reply_id, self.id)
end
board.reset_counters!
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)
else
board.increment! :topics_count
end
end
def after_destroy
board.reset_counters!
end
def sticky=(arg)
write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
# The following line is required so that the previous counter
# updates (due to children removal) are not overwritten
board.reload
board.decrement! :messages_count
board.decrement! :topics_count unless parent
end
def sticky?
@@ -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)))
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
def add_author_as_watcher

View File

@@ -17,6 +17,14 @@
class MessageObserver < ActiveRecord::Observer
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

View File

@@ -24,22 +24,11 @@ class News < ActiveRecord::Base
validates_length_of :title, :maximum => 60
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_activity_provider :find_options => {:include => [:project, :author]},
: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
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")

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

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 'users'
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

View File

@@ -20,16 +20,8 @@ class Project < ActiveRecord::Base
STATUS_ACTIVE = 1
STATUS_ARCHIVED = 9
# Specific overidden Activities
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 :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
has_many :users, :through => :members
has_many :principals, :through => :member_principals, :source => :principal
has_many :enabled_modules, :dependent => :delete_all
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
@@ -51,7 +43,7 @@ class Project < ActiveRecord::Base
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
: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,
:delete_permission => :manage_files
@@ -68,17 +60,14 @@ class Project < ActiveRecord::Base
validates_associated :repository, :wiki
validates_length_of :name, :maximum => 30
validates_length_of :homepage, :maximum => 255
validates_length_of :identifier, :in => 1..20
# donwcase letters, digits, dashes but not digits only
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
validates_length_of :identifier, :in => 2..20
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
before_destroy :delete_all_members
named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
named_scope :all_public, { :conditions => { :is_public => true } }
named_scope :public, { :conditions => { :is_public => true } }
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
def identifier=(identifier)
@@ -88,6 +77,21 @@ class Project < ActiveRecord::Base
def identifier_frozen?
errors[:identifier].nil? && !(new_record? || identifier.blank?)
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
# non public projects will be returned only if user is a member of those
@@ -95,11 +99,6 @@ class Project < ActiveRecord::Base
find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
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)
user ||= User.current
if user && user.admin?
@@ -117,7 +116,7 @@ class Project < ActiveRecord::Base
if perm = Redmine::AccessControl.permission(permission)
unless perm.project_module.nil?
# If the permission belongs to a project module, make sure the module is enabled
base_statement << " AND #{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
if options[:project]
@@ -130,71 +129,19 @@ class Project < ActiveRecord::Base
else
statements << "1=0"
if user.logged?
if Role.non_member.allowed_to?(permission) && !options[:member]
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
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}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
elsif Role.anonymous.allowed_to?(permission)
# anonymous user allowed on public project
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
else
if Role.anonymous.allowed_to?(permission) && !options[:member]
# anonymous user allowed on public project
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
end
# anonymous user is not authorized
end
end
statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
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)
cond = "#{Project.table_name}.id = #{id}"
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
end
# Archives the project and its descendants
# Archives the project and its descendants recursively
def archive
# Check that there is no issue of a non descendant project that is assigned
# to one of the project or descendant versions
v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
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
# Archive subprojects if any
children.each do |subproject|
subproject.archive
end
Project.transaction do
archive!
end
true
update_attribute :status, STATUS_ARCHIVED
end
# Unarchives the project
@@ -244,38 +184,8 @@ class Project < ActiveRecord::Base
end
# Returns an array of projects the project can be moved to
# by the current user
def allowed_parents
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)
@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)
def possible_parents
@possible_parents ||= (Project.active.find(:all) - self_and_descendants)
end
# 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(p)
end
Issue.update_versions_from_hierarchy_change(self)
true
else
# Can not move to the given target
@@ -326,51 +235,14 @@ class Project < ActiveRecord::Base
:order => "#{Tracker.table_name}.position")
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
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])
end
# Users issues can be assigned to
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
# 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}
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
# (explictly associated custom fields and custom fields enabled for all projects)
def all_issue_custom_fields
@@ -406,10 +273,6 @@ class Project < ActiveRecord::Base
description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
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)
if action.is_a? Hash
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
@@ -424,14 +287,10 @@ class Project < ActiveRecord::Base
end
def enabled_module_names=(module_names)
if module_names && module_names.is_a?(Array)
module_names = module_names.collect(&:to_s)
# remove disabled modules
enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
# add new modules
module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
else
enabled_modules.clear
enabled_modules.clear
module_names = [] unless module_names && module_names.is_a?(Array)
module_names.each do |name|
enabled_modules << EnabledModule.new(:name => name.to_s)
end
end
@@ -441,190 +300,12 @@ class Project < ActiveRecord::Base
p.nil? ? nil : p.identifier.to_s.succ
end
# Copies and saves the Project instance based on the +project+.
# Duplicates the source project's:
# * Wiki
# * 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")
project.wiki.pages.each do |page|
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
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
protected
def validate
errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
end
private
def allowed_permissions
@allowed_permissions ||= begin
module_names = enabled_modules.collect {|m| m.name}
@@ -635,50 +316,4 @@ class Project < ActiveRecord::Base
def allowed_actions
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
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

View File

@@ -16,31 +16,19 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class QueryColumn
attr_accessor :name, :sortable, :groupable, :default_order
include Redmine::I18n
attr_accessor :name, :sortable, :default_order
include GLoc
def initialize(name, options={})
self.name = name
self.sortable = options[:sortable]
self.groupable = options[:groupable] || false
if groupable == true
self.groupable = name.to_s
end
self.default_order = options[:default_order]
end
def caption
set_language_if_valid(User.current.language)
l("field_#{name}")
end
# Returns true if the column is sortable, otherwise false
def sortable?
!sortable.nil?
end
def value(issue)
issue.send name
end
end
class QueryCustomFieldColumn < QueryColumn
@@ -48,10 +36,6 @@ class QueryCustomFieldColumn < QueryColumn
def initialize(custom_field)
self.name = "cf_#{custom_field.id}".to_sym
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
end
@@ -62,22 +46,13 @@ class QueryCustomFieldColumn < QueryColumn
def custom_field
@cf
end
def value(issue)
cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
cv && @cf.cast_value(cv.value)
end
end
class Query < ActiveRecord::Base
class StatementInvalid < ::ActiveRecord::StatementInvalid
end
belongs_to :project
belongs_to :user
serialize :filters
serialize :column_names
serialize :sort_criteria, Array
attr_protected :project_id, :user_id
@@ -90,8 +65,8 @@ class Query < ActiveRecord::Base
"c" => :label_closed_issues,
"!*" => :label_none,
"*" => :label_all,
">=" => :label_greater_or_equal,
"<=" => :label_less_or_equal,
">=" => '>=',
"<=" => '<=',
"<t+" => :label_in_less_than,
">t+" => :label_in_more_than,
"t+" => :label_in,
@@ -118,20 +93,19 @@ class Query < ActiveRecord::Base
cattr_reader :operators_by_filter_type
@@available_columns = [
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
QueryColumn.new(:author),
QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{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(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :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'),
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :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'),
]
cattr_reader :available_columns
@@ -139,6 +113,7 @@ class Query < ActiveRecord::Base
def initialize(attributes = nil)
super attributes
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
set_language_if_valid(User.current.language)
end
def after_initialize
@@ -148,7 +123,7 @@ class Query < ActiveRecord::Base
def validate
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
(values_for(field) and !values_for(field).first.blank?) or
# 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] } },
"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 },
"created_on" => { :type => :date_past, :order => 9 },
"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] }
else
# 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] }
end
@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?
@available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
end
unless @project.shared_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] } }
unless @project.versions.empty?
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
end
unless @project.descendants.active.empty?
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
@@ -232,7 +206,7 @@ class Query < ActiveRecord::Base
def add_short_filter(field, expression)
return unless expression
parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
add_filter field, (parms[0] || "="), [parms[1] || ""]
end
@@ -258,21 +232,13 @@ class Query < ActiveRecord::Base
@available_columns = Query.available_columns
@available_columns += (project ?
project.all_issue_custom_fields :
IssueCustomField.find(:all)
IssueCustomField.find(:all, :conditions => {:is_for_all => true})
).collect {|cf| QueryCustomFieldColumn.new(cf) }
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
if has_default_columns?
available_columns.select do |c|
# 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
available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
else
# preserve the column_names order
column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
@@ -280,14 +246,8 @@ class Query < ActiveRecord::Base
end
def column_names=(names)
if names
names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
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
names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
write_attribute(:column_names, names)
end
@@ -299,49 +259,6 @@ class Query < ActiveRecord::Base
column_names.nil? || column_names.empty?
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
project_clauses = []
if project && !@project.descendants.active.empty?
@@ -408,69 +325,6 @@ class Query < ActiveRecord::Base
(filters_clauses << project_statement).join(' AND ')
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
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
@@ -517,9 +371,9 @@ class Query < ActiveRecord::Base
Time.now.at_beginning_of_week
sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when "~"
sql = "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 "!~"
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
return sql

View File

@@ -1,3 +1,4 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
@@ -19,14 +20,14 @@ class Repository < ActiveRecord::Base
belongs_to :project
has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
has_many :changes, :through => :changesets
# Raw SQL to delete changesets and changes in the database
# has_many :changesets, :dependent => :destroy is too slow for big repositories
before_destroy :clear_changesets
# 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
def url=(arg)
write_attribute(:url, arg ? arg.to_s.strip : nil)
@@ -38,7 +39,8 @@ class Repository < ActiveRecord::Base
end
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?
@scm
end
@@ -62,18 +64,6 @@ class Repository < ActiveRecord::Base
def entries(path=nil, identifier=nil)
scm.entries(path, identifier)
end
def branches
scm.branches
end
def tags
scm.tags
end
def default_branch
scm.default_branch
end
def properties(path, identifier=nil)
scm.properties(path, identifier)
@@ -87,39 +77,28 @@ class Repository < ActiveRecord::Base
scm.diff(path, rev, rev_to)
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
def relative_path(path)
path
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
@latest_changeset ||= changesets.find(:first)
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
self.changesets.each(&:scan_comment_for_issue_ids)
end
# Returns an array of committers usernames and associated user_id
def committers
@committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
@@ -185,9 +164,31 @@ class Repository < ActiveRecord::Base
rescue
nil
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
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
# Strips url and root_url
url.strip!
@@ -196,9 +197,8 @@ class Repository < ActiveRecord::Base
end
def clear_changesets
cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.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 #{cs} WHERE #{cs}.repository_id = #{id}")
connection.delete("DELETE FROM changes WHERE changes.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.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 changesets WHERE changesets.repository_id = #{id}")
end
end

View File

@@ -21,6 +21,17 @@ class Repository::Git < Repository
attr_protected :root_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
Redmine::Scm::Adapters::GitAdapter
end
@@ -29,50 +40,45 @@ class Repository::Git < Repository
'Git'
end
def branches
scm.branches
def changesets_for_path(path, options={})
Change.find(:all, :include => {:changeset => :user},
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC",
:limit => options[:limit]).collect(&:changeset)
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 always have to
# parse the entire log.
def fetch_changesets
# Save ourselves an expensive operation if we're already up to date
return if scm.num_revisions == changesets.count
create_or_sync_cache if cache
revisions = scm.revisions('', nil, nil, :all => true)
return if revisions.nil? || revisions.empty?
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision : nil
# latest revision in the repository
scm_revision = scm_info.lastrev.scmid
# Find revisions that redmine knows about already
existing_revisions = changesets.find(:all).map!{|c| c.scmid}
# Clean out revisions that are no longer in git
Changeset.delete_all(["scmid NOT IN (?) AND repository_id = (?)", revisions.map{|r| r.scmid}, self.id])
# Subtract revisions that redmine already knows about
revisions.reject!{|r| existing_revisions.include?(r.scmid)}
# Save the remaining ones to the database
revisions.each{|r| r.save(self)} unless revisions.nil?
end
def latest_changesets(path,rev,limit=10)
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'
)
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)
revision.paths.each do |change|
Change.create!(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
end
end
end
end
end
end
end

View File

@@ -21,6 +21,15 @@ class Repository::Mercurial < Repository
attr_protected :root_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
Redmine::Scm::Adapters::MercurialAdapter
end
@@ -53,6 +62,8 @@ class Repository::Mercurial < Repository
end
def fetch_changesets
create_or_sync_cache if cache
scm_info = scm.info
if scm_info
# latest revision found in database

View File

@@ -20,7 +20,17 @@ require 'redmine/scm/adapters/subversion_adapter'
class Repository::Subversion < Repository
attr_protected :root_url
validates_presence_of :url
validates_format_of :url, :with => /^(http|https|svn(\+[^\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
Redmine::Scm::Adapters::SubversionAdapter
@@ -30,8 +40,8 @@ class Repository::Subversion < Repository
'Subversion'
end
def latest_changesets(path, rev, limit=10)
revisions = scm.revisions(path, rev, nil, :limit => limit)
def changesets_for_path(path, options={})
revisions = scm.revisions(path, nil, nil, :limit => options[:limit])
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
end
@@ -41,6 +51,8 @@ class Repository::Subversion < Repository
end
def fetch_changesets
create_or_sync_cache if cache
scm_info = scm.info
if scm_info
# latest revision found in database
@@ -54,8 +66,8 @@ class Repository::Subversion < Repository
# loads changesets by batches of 200
identifier_to = [identifier_from + 199, scm_revision].min
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,
:revision => revision.identifier,
:committer => revision.author,
@@ -68,7 +80,7 @@ class Repository::Subversion < Repository
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end unless changeset.new_record?
end
end
end unless revisions.nil?
identifier_from = identifier_to + 1
@@ -84,6 +96,6 @@ class Repository::Subversion < Repository
# url = file:///var/svn/foo/bar
# => returns /bar
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

View File

@@ -20,7 +20,6 @@ class Role < ActiveRecord::Base
BUILTIN_NON_MEMBER = 1
BUILTIN_ANONYMOUS = 2
named_scope :givable, { :conditions => "builtin = 0", :order => 'position' }
named_scope :builtin, lambda { |*args|
compare = 'not' if args.first == true
{ :conditions => "#{compare} builtin = 0" }
@@ -28,13 +27,18 @@ class Role < ActiveRecord::Base
before_destroy :check_deletable
has_many :workflows, :dependent => :delete_all do
def copy(source_role)
Workflow.copy(nil, source_role, nil, proxy_owner)
def copy(role)
raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role)
raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record?
clear
connection.insert "INSERT INTO #{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
has_many :member_roles, :dependent => :destroy
has_many :members, :through => :member_roles
has_many :members
acts_as_list
serialize :permissions, Array
@@ -78,11 +82,7 @@ class Role < ActiveRecord::Base
end
def <=>(role)
role ? position <=> role.position : -1
end
def to_s
name
position <=> role.position
end
# Return true if the role is a builtin role

View File

@@ -141,7 +141,7 @@ class Setting < ActiveRecord::Base
end
def self.openid?
Object.const_defined?(:OpenID) && self[:openid].to_i > 0
Object.const_defined?(:OpenID) && self['openid'].to_s == '1'
end
# Checks if settings have changed since the values were read
@@ -165,4 +165,14 @@ private
setting = find_by_name(name)
setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name
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

View File

@@ -21,30 +21,25 @@ class TimeEntry < ActiveRecord::Base
belongs_to :project
belongs_to :issue
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
acts_as_customizable
acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
:url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project, :issue_id => o.issue}},
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}},
:author => :user,
: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_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
def after_initialize
if new_record? && self.activity.nil?
if default_activity = TimeEntryActivity.default
if default_activity = Enumeration.default('ACTI')
self.activity_id = default_activity.id
end
self.hours = nil if hours == 0
end
end
@@ -53,9 +48,9 @@ class TimeEntry < ActiveRecord::Base
end
def validate
errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
errors.add :project_id, :invalid if project.nil?
errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000)
errors.add :project_id, :activerecord_error_invalid if project.nil?
errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
end
def hours=(h)

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

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 TimeEntryActivityCustomField < CustomField
def type_name
:enumeration_activities
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -17,9 +17,6 @@
class Token < ActiveRecord::Base
belongs_to :user
validates_uniqueness_of :value
before_create :delete_previous_tokens
@@validity_time = 1.day
@@ -39,13 +36,9 @@ class Token < ActiveRecord::Base
private
def self.generate_token_value
ActiveSupport::SecureRandom.hex(20)
end
# Removes obsolete tokens (same user and action)
def delete_previous_tokens
if user
Token.delete_all(['user_id = ? AND action = ?', user.id, action])
end
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
token_value = ''
40.times { |i| token_value << chars[rand(chars.size-1)] }
token_value
end
end

View File

@@ -19,8 +19,14 @@ class Tracker < ActiveRecord::Base
before_destroy :check_integrity
has_many :issues
has_many :workflows, :dependent => :delete_all do
def copy(source_tracker)
Workflow.copy(source_tracker, nil, proxy_owner, nil)
def copy(tracker)
raise "Can not copy workflow from a #{tracker.class}" unless tracker.is_a?(Tracker)
raise "Can not copy workflow from/to an unsaved tracker" if proxy_owner.new_record? || tracker.new_record?
clear
connection.insert "INSERT INTO #{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
@@ -43,23 +49,6 @@ class Tracker < ActiveRecord::Base
find(:all, :order => 'position')
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
def check_integrity
raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -17,7 +17,7 @@
require "digest/sha1"
class User < Principal
class User < ActiveRecord::Base
# Account statuses
STATUS_ANONYMOUS = 0
@@ -33,13 +33,13 @@ class User < Principal
:username => '#{login}'
}
has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
:after_remove => Proc.new {|user, group| group.user_removed(user)}
has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
has_many :members, :dependent => :delete_all
has_many :projects, :through => :memberships
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
has_many :changesets, :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
belongs_to :auth_source
# Active non-anonymous users scope
@@ -50,7 +50,7 @@ class User < Principal
attr_accessor :password, :password_confirmation
attr_accessor :last_before_login_on
# 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_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_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
validates_length_of :mail, :maximum => 60, :allow_nil => true
validates_length_of :password, :minimum => 4, :allow_nil => true
validates_confirmation_of :password, :allow_nil => true
def before_create
@@ -80,14 +81,10 @@ class User < Principal
end
def identity_url=(url)
if url.blank?
write_attribute(:identity_url, '')
else
begin
write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
rescue OpenIdAuthentication::InvalidOpenId
# Invlaid url, don't save
end
begin
self.write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
rescue InvalidOpenId
# Invlaid url, don't save
end
self.read_attribute(:identity_url)
end
@@ -125,19 +122,6 @@ class User < Principal
rescue => text
raise text
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
def name(formatter = nil)
@@ -193,12 +177,6 @@ class User < Principal
token = self.rss_token || Token.create(:user => self, :action => 'feeds')
token.value
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
def notified_projects_ids
@@ -217,29 +195,25 @@ class User < Principal
token && token.user.active? ? token.user : nil
end
def self.find_by_api_key(key)
token = Token.find_by_action_and_value('api', key)
token && token.user.active? ? token.user : nil
def self.find_by_autologin_key(key)
token = Token.find_by_action_and_value('autologin', key)
token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
end
# Makes find_by_mail case-insensitive
def self.find_by_mail(mail)
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
end
# Sort users by their display names
def <=>(user)
self.to_s.downcase <=> user.to_s.downcase
end
def to_s
name
end
# Returns the current day according to user's time zone
def today
if time_zone.nil?
Date.today
else
Time.now.in_time_zone(time_zone).to_date
end
end
def logged?
true
end
@@ -248,30 +222,26 @@ class User < Principal
!logged?
end
# Return user's roles for project
def roles_for_project(project)
roles = []
# Return user's role for project
def role_for_project(project)
# No role on archived projects
return roles unless project && project.active?
return nil unless project && project.active?
if logged?
# Find project membership
membership = memberships.detect {|m| m.project_id == project.id}
if membership
roles = membership.roles
membership.role
else
@role_non_member ||= Role.non_member
roles << @role_non_member
end
else
@role_anonymous ||= Role.anonymous
roles << @role_anonymous
end
roles
end
# Return true if the user is a member of project
def member_of?(project)
!roles_for_project(project).detect {|role| role.member?}.nil?
role_for_project(project).member?
end
# 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
return true if admin?
roles = roles_for_project(project)
return false unless roles
roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
role = role_for_project(project)
return false unless role
role.allowed_to?(action) && (project.is_public? || role.member?)
elsif options[:global]
# Admin users are always authorized
return true if admin?
# 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))
else
false
@@ -311,8 +278,6 @@ class User < Principal
@current_user ||= User.anonymous
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
anonymous_user = AnonymousUser.find(:first)
if anonymous_user.nil?
@@ -322,17 +287,7 @@ class User < Principal
anonymous_user
end
protected
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
private
# Return password digest
def self.hash_password(clear_password)
Digest::SHA1.hexdigest(clear_password || "")
@@ -353,7 +308,7 @@ class AnonymousUser < User
# Overrides a few properties
def logged?; false end
def admin; false end
def name(*args); I18n.t(:label_user_anonymous) end
def name; 'Anonymous' end
def mail; nil end
def time_zone; nil end
def rss_key; nil end

View File

@@ -17,31 +17,15 @@
class Version < ActiveRecord::Base
before_destroy :check_integrity
after_update :update_issues_from_sharing_change
belongs_to :project
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
acts_as_customizable
acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files
VERSION_STATUSES = %w(open locked closed)
VERSION_SHARINGS = %w(none descendants hierarchy tree system)
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]
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_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
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => 'activerecord_error_not_a_date', :allow_nil => true
def start_date
effective_date
@@ -61,21 +45,11 @@ class Version < ActiveRecord::Base
@spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
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
def completed?
effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
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
if issues_count == 0
0
@@ -86,7 +60,6 @@ class Version < ActiveRecord::Base
end
end
# Returns the percentage of issues that have been marked as 'closed'.
def closed_pourcent
if issues_count == 0
0
@@ -105,12 +78,10 @@ class Version < ActiveRecord::Base
@issue_count ||= fixed_issues.count
end
# Returns the total amount of open issues for this version.
def open_issues_count
@open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
end
# Returns the total amount of closed issues for this version.
def closed_issues_count
@closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
end
@@ -134,42 +105,10 @@ class Version < ActiveRecord::Base
end
end
# Returns the sharings that +user+ can set the version to
def allowed_sharings(user = User.current)
VERSION_SHARINGS.select do |s|
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
def check_integrity
raise "Can't delete version" if self.fixed_issues.find(:first)
end
# 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
# Returns the average estimated time of assigned issues
# or 1 if no issue has an estimated time
@@ -185,22 +124,17 @@ private
@estimated_average
end
# Returns the total progress of open or closed issues. The returned percentage takes into account
# 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.
# Returns the total progress of open or closed issues
def issues_progress(open)
@issues_progress ||= {}
@issues_progress[open] ||= begin
progress = 0
if issues_count > 0
ratio = open ? 'done_ratio' : 100
done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
:include => :status,
:conditions => ["is_closed = ?", !open]).to_f
progress = done / (estimated_average * issues_count)
end
progress

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

View File

@@ -21,45 +21,10 @@ class Watcher < ActiveRecord::Base
validates_presence_of :user
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
def validate
errors.add :user_id, :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
errors.add :user_id, :activerecord_error_invalid unless user.nil? || user.active?
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -20,15 +20,9 @@ class Wiki < ActiveRecord::Base
has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
acts_as_watchable
validates_presence_of :start_page
validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_wiki_pages, project)
end
# find the page with the given title
# if page doesn't exist, return a new page
def find_or_new_page(title)

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