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-8f06a7374b81nbc
@@ -1,5 +1,5 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006-2007 Jean-Philippe Lang | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2008 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
@@ -24,13 +24,17 @@ class AccountController < ApplicationController | |||
# Show user's account | |||
def show | |||
@user = User.find_active(params[:id]) | |||
@user = User.active.find(params[:id]) | |||
@custom_values = @user.custom_values | |||
# show only public projects and private projects that the logged in user is also a member of | |||
@memberships = @user.memberships.select do |membership| | |||
membership.project.is_public? || (User.current.member_of?(membership.project)) | |||
end | |||
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10) | |||
@events_by_day = events.group_by(&:event_date) | |||
rescue ActiveRecord::RecordNotFound | |||
render_404 | |||
end | |||
@@ -42,24 +46,10 @@ class AccountController < ApplicationController | |||
self.logged_user = nil | |||
else | |||
# Authenticate user | |||
user = User.try_to_login(params[:username], params[:password]) | |||
if user.nil? | |||
# Invalid credentials | |||
flash.now[:error] = l(:notice_account_invalid_creditentials) | |||
elsif user.new_record? | |||
# Onthefly creation failed, display the registration form to fill/fix attributes | |||
@user = user | |||
session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id } | |||
render :action => 'register' | |||
if Setting.openid? && using_open_id? | |||
open_id_authenticate(params[:openid_url]) | |||
else | |||
# Valid user | |||
self.logged_user = user | |||
# generate a key and set cookie if autologin | |||
if params[:autologin] && Setting.autologin? | |||
token = Token.create(:user => user, :action => 'autologin') | |||
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now } | |||
end | |||
redirect_back_or_default :controller => 'my', :action => 'page' | |||
password_authentication | |||
end | |||
end | |||
end | |||
@@ -132,31 +122,14 @@ class AccountController < ApplicationController | |||
else | |||
@user.login = params[:user][:login] | |||
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] | |||
case Setting.self_registration | |||
when '1' | |||
# Email activation | |||
token = Token.new(:user => @user, :action => "register") | |||
if @user.save and token.save | |||
Mailer.deliver_register(token) | |||
flash[:notice] = l(:notice_account_register_done) | |||
redirect_to :action => 'login' | |||
end | |||
register_by_email_activation(@user) | |||
when '3' | |||
# Automatic activation | |||
@user.status = User::STATUS_ACTIVE | |||
if @user.save | |||
self.logged_user = @user | |||
flash[:notice] = l(:notice_account_activated) | |||
redirect_to :controller => 'my', :action => 'account' | |||
end | |||
register_automatically(@user) | |||
else | |||
# Manual activation by the administrator | |||
if @user.save | |||
# Sends an email to the administrators | |||
Mailer.deliver_account_activation_request(@user) | |||
flash[:notice] = l(:notice_account_pending) | |||
redirect_to :action => 'login' | |||
end | |||
register_manually_by_administrator(@user) | |||
end | |||
end | |||
end | |||
@@ -187,4 +160,119 @@ private | |||
session[:user_id] = nil | |||
end | |||
end | |||
def password_authentication | |||
user = User.try_to_login(params[:username], params[:password]) | |||
if user.nil? | |||
# Invalid credentials | |||
flash.now[:error] = l(:notice_account_invalid_creditentials) | |||
elsif user.new_record? | |||
# Onthefly creation failed, display the registration form to fill/fix attributes | |||
@user = user | |||
session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id } | |||
render :action => 'register' | |||
else | |||
# Valid user | |||
successful_authentication(user) | |||
end | |||
end | |||
def open_id_authenticate(openid_url) | |||
authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url) do |result, identity_url, registration| | |||
if result.successful? | |||
user = User.find_or_initialize_by_identity_url(identity_url) | |||
if user.new_record? | |||
# Self-registration off | |||
redirect_to(home_url) && return unless Setting.self_registration? | |||
# Create on the fly | |||
user.login = registration['nickname'] unless registration['nickname'].nil? | |||
user.mail = registration['email'] unless registration['email'].nil? | |||
user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil? | |||
user.random_password | |||
user.status = User::STATUS_REGISTERED | |||
case Setting.self_registration | |||
when '1' | |||
register_by_email_activation(user) do | |||
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url }) | |||
end | |||
when '3' | |||
register_automatically(user) do | |||
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url }) | |||
end | |||
else | |||
register_manually_by_administrator(user) do | |||
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url }) | |||
end | |||
end | |||
else | |||
# Existing record | |||
successful_authentication(user) | |||
end | |||
end | |||
end | |||
end | |||
def successful_authentication(user) | |||
# Valid user | |||
self.logged_user = user | |||
# generate a key and set cookie if autologin | |||
if params[:autologin] && Setting.autologin? | |||
token = Token.create(:user => user, :action => 'autologin') | |||
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now } | |||
end | |||
redirect_back_or_default :controller => 'my', :action => 'page' | |||
end | |||
# Onthefly creation failed, display the registration form to fill/fix attributes | |||
def onthefly_creation_failed(user, auth_source_options = { }) | |||
@user = user | |||
session[:auth_source_registration] = auth_source_options unless auth_source_options.empty? | |||
render :action => 'register' | |||
end | |||
# Register a user for email activation. | |||
# | |||
# Pass a block for behavior when a user fails to save | |||
def register_by_email_activation(user, &block) | |||
token = Token.new(:user => user, :action => "register") | |||
if user.save and token.save | |||
Mailer.deliver_register(token) | |||
flash[:notice] = l(:notice_account_register_done) | |||
redirect_to :action => 'login' | |||
else | |||
yield if block_given? | |||
end | |||
end | |||
# Automatically register a user | |||
# | |||
# Pass a block for behavior when a user fails to save | |||
def register_automatically(user, &block) | |||
# Automatic activation | |||
user.status = User::STATUS_ACTIVE | |||
if user.save | |||
self.logged_user = user | |||
flash[:notice] = l(:notice_account_activated) | |||
redirect_to :controller => 'my', :action => 'account' | |||
else | |||
yield if block_given? | |||
end | |||
end | |||
# Manual activation by the administrator | |||
# | |||
# Pass a block for behavior when a user fails to save | |||
def register_manually_by_administrator(user, &block) | |||
if user.save | |||
# Sends an email to the administrators | |||
Mailer.deliver_account_activation_request(user) | |||
flash[:notice] = l(:notice_account_pending) | |||
redirect_to :action => 'login' | |||
else | |||
yield if block_given? | |||
end | |||
end | |||
end |
@@ -26,25 +26,24 @@ class AdminController < ApplicationController | |||
end | |||
def projects | |||
sort_init 'name', 'asc' | |||
sort_update | |||
@status = params[:status] ? params[:status].to_i : 1 | |||
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) | |||
@status = params[:status] ? params[:status].to_i : 0 | |||
conditions = nil | |||
conditions = ["status=?", @status] unless @status == 0 | |||
unless params[:name].blank? | |||
name = "%#{params[:name].strip.downcase}%" | |||
c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name] | |||
end | |||
@project_count = Project.count(:conditions => conditions) | |||
@project_pages = Paginator.new self, @project_count, | |||
per_page_option, | |||
params['page'] | |||
@projects = Project.find :all, :order => sort_clause, | |||
:conditions => conditions, | |||
:limit => @project_pages.items_per_page, | |||
:offset => @project_pages.current.offset | |||
@projects = Project.find :all, :order => 'lft', | |||
:conditions => c.conditions | |||
render :action => "projects", :layout => false if request.xhr? | |||
end | |||
def plugins | |||
@plugins = Redmine::Plugin.all | |||
end | |||
# Loads the default configuration | |||
# (roles, trackers, statuses, workflow, enumerations) | |||
def default_configuration | |||
@@ -78,8 +77,8 @@ class AdminController < ApplicationController | |||
@flags = { | |||
:default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?, | |||
:file_repository_writable => File.writable?(Attachment.storage_path), | |||
:plugin_assets_writable => File.writable?(Engines.public_directory), | |||
:rmagick_available => Object.const_defined?(:Magick) | |||
} | |||
@plugins = Redmine::Plugin.registered_plugins | |||
end | |||
end |
@@ -46,7 +46,7 @@ class ApplicationController < ActionController::Base | |||
def find_current_user | |||
if session[:user_id] | |||
# existing session | |||
(User.find_active(session[:user_id]) rescue nil) | |||
(User.active.find(session[:user_id]) rescue nil) | |||
elsif cookies[:autologin] && Setting.autologin? | |||
# auto-login feature | |||
User.find_by_autologin_key(cookies[:autologin]) | |||
@@ -82,7 +82,7 @@ class ApplicationController < ActionController::Base | |||
def require_login | |||
if !User.current.logged? | |||
redirect_to :controller => "account", :action => "login", :back_url => (request.relative_url_root + request.request_uri) | |||
redirect_to :controller => "account", :action => "login", :back_url => url_for(params) | |||
return false | |||
end | |||
true | |||
@@ -126,10 +126,14 @@ class ApplicationController < ActionController::Base | |||
def redirect_back_or_default(default) | |||
back_url = CGI.unescape(params[:back_url].to_s) | |||
if !back_url.blank? | |||
uri = URI.parse(back_url) | |||
# do not redirect user to another host | |||
if uri.relative? || (uri.host == request.host) | |||
redirect_to(back_url) and return | |||
begin | |||
uri = URI.parse(back_url) | |||
# do not redirect user to another host or to the login or register page | |||
if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)}) | |||
redirect_to(back_url) and return | |||
end | |||
rescue URI::InvalidURIError | |||
# redirect to default | |||
end | |||
end | |||
redirect_to default | |||
@@ -171,6 +175,7 @@ class ApplicationController < ActionController::Base | |||
# TODO: move to model | |||
def attach_files(obj, attachments) | |||
attached = [] | |||
unsaved = [] | |||
if attachments && attachments.is_a?(Hash) | |||
attachments.each_value do |attachment| | |||
file = attachment['file'] | |||
@@ -179,7 +184,10 @@ class ApplicationController < ActionController::Base | |||
:file => file, | |||
:description => attachment['description'].to_s.strip, | |||
:author => User.current) | |||
attached << a unless a.new_record? | |||
a.new_record? ? (unsaved << a) : (attached << a) | |||
end | |||
if unsaved.any? | |||
flash[:warning] = l(:warning_attachments_not_saved, unsaved.size) | |||
end | |||
end | |||
attached |
@@ -1,5 +1,5 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006-2007 Jean-Philippe Lang | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2008 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
@@ -17,7 +17,11 @@ | |||
class AttachmentsController < ApplicationController | |||
before_filter :find_project | |||
before_filter :read_authorize, :except => :destroy | |||
before_filter :delete_authorize, :only => :destroy | |||
verify :method => :post, :only => :destroy | |||
def show | |||
if @attachment.is_diff? | |||
@diff = File.new(@attachment.diskfile, "rb").read | |||
@@ -25,31 +29,46 @@ class AttachmentsController < ApplicationController | |||
elsif @attachment.is_text? | |||
@content = File.new(@attachment.diskfile, "rb").read | |||
render :action => 'file' | |||
elsif | |||
else | |||
download | |||
end | |||
end | |||
def download | |||
@attachment.increment_download if @attachment.container.is_a?(Version) | |||
if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project) | |||
@attachment.increment_download | |||
end | |||
# images are sent inline | |||
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), | |||
:type => @attachment.content_type, | |||
:disposition => (@attachment.image? ? 'inline' : 'attachment') | |||
end | |||
def destroy | |||
# Make sure association callbacks are called | |||
@attachment.container.attachments.delete(@attachment) | |||
redirect_to :back | |||
rescue ::ActionController::RedirectBackError | |||
redirect_to :controller => 'projects', :action => 'show', :id => @project | |||
end | |||
private | |||
def find_project | |||
@attachment = Attachment.find(params[:id]) | |||
# Show 404 if the filename in the url is wrong | |||
raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename | |||
@project = @attachment.project | |||
permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym | |||
allowed = User.current.allowed_to?(permission, @project) | |||
allowed ? true : (User.current.logged? ? render_403 : require_login) | |||
rescue ActiveRecord::RecordNotFound | |||
render_404 | |||
end | |||
def read_authorize | |||
@attachment.visible? ? true : deny_access | |||
end | |||
def delete_authorize | |||
@attachment.deletable? ? true : deny_access | |||
end | |||
end |
@@ -35,12 +35,14 @@ class BoardsController < ApplicationController | |||
end | |||
def show | |||
sort_init "#{Message.table_name}.updated_on", "desc" | |||
sort_update | |||
sort_init 'updated_on', 'desc' | |||
sort_update 'created_on' => "#{Message.table_name}.created_on", | |||
'replies' => "#{Message.table_name}.replies_count", | |||
'updated_on' => "#{Message.table_name}.updated_on" | |||
@topic_count = @board.topics.count | |||
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page'] | |||
@topics = @board.topics.find :all, :order => "#{Message.table_name}.sticky DESC, #{sort_clause}", | |||
@topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '), | |||
:include => [:author, {:last_reply => :author}], | |||
:limit => @topic_pages.items_per_page, | |||
:offset => @topic_pages.current.offset |
@@ -1,5 +1,5 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006 Jean-Philippe Lang | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2009 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
@@ -19,34 +19,22 @@ class CustomFieldsController < ApplicationController | |||
before_filter :require_admin | |||
def index | |||
list | |||
render :action => 'list' unless request.xhr? | |||
end | |||
def list | |||
@custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name } | |||
@tab = params[:tab] || 'IssueCustomField' | |||
render :action => "list", :layout => false if request.xhr? | |||
end | |||
def new | |||
case params[:type] | |||
when "IssueCustomField" | |||
@custom_field = IssueCustomField.new(params[:custom_field]) | |||
@custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids] | |||
when "UserCustomField" | |||
@custom_field = UserCustomField.new(params[:custom_field]) | |||
when "ProjectCustomField" | |||
@custom_field = ProjectCustomField.new(params[:custom_field]) | |||
when "TimeEntryCustomField" | |||
@custom_field = TimeEntryCustomField.new(params[:custom_field]) | |||
else | |||
redirect_to :action => 'list' | |||
return | |||
end | |||
@custom_field = begin | |||
if params[:type].to_s.match(/.+CustomField$/) | |||
params[:type].to_s.constantize.new(params[:custom_field]) | |||
end | |||
rescue | |||
end | |||
redirect_to(:action => 'index') and return unless @custom_field.is_a?(CustomField) | |||
if request.post? and @custom_field.save | |||
flash[:notice] = l(:notice_successful_create) | |||
redirect_to :action => 'list', :tab => @custom_field.class.name | |||
redirect_to :action => 'index', :tab => @custom_field.class.name | |||
end | |||
@trackers = Tracker.find(:all, :order => 'position') | |||
end | |||
@@ -54,11 +42,8 @@ class CustomFieldsController < ApplicationController | |||
def edit | |||
@custom_field = CustomField.find(params[:id]) | |||
if request.post? and @custom_field.update_attributes(params[:custom_field]) | |||
if @custom_field.is_a? IssueCustomField | |||
@custom_field.trackers = params[:tracker_ids] ? Tracker.find(params[:tracker_ids]) : [] | |||
end | |||
flash[:notice] = l(:notice_successful_update) | |||
redirect_to :action => 'list', :tab => @custom_field.class.name | |||
redirect_to :action => 'index', :tab => @custom_field.class.name | |||
end | |||
@trackers = Tracker.find(:all, :order => 'position') | |||
end | |||
@@ -75,14 +60,14 @@ class CustomFieldsController < ApplicationController | |||
when 'lowest' | |||
@custom_field.move_to_bottom | |||
end if params[:position] | |||
redirect_to :action => 'list', :tab => @custom_field.class.name | |||
redirect_to :action => 'index', :tab => @custom_field.class.name | |||
end | |||
def destroy | |||
@custom_field = CustomField.find(params[:id]).destroy | |||
redirect_to :action => 'list', :tab => @custom_field.class.name | |||
redirect_to :action => 'index', :tab => @custom_field.class.name | |||
rescue | |||
flash[:error] = "Unable to delete custom field" | |||
redirect_to :action => 'list' | |||
redirect_to :action => 'index' | |||
end | |||
end |
@@ -35,6 +35,7 @@ class DocumentsController < ApplicationController | |||
else | |||
@grouped = documents.group_by(&:category) | |||
end | |||
@document = @project.documents.build | |||
render :layout => false if request.xhr? | |||
end | |||
@@ -70,11 +71,6 @@ class DocumentsController < ApplicationController | |||
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added') | |||
redirect_to :action => 'show', :id => @document | |||
end | |||
def destroy_attachment | |||
@document.attachments.find(params[:attachment_id]).destroy | |||
redirect_to :action => 'show', :id => @document | |||
end | |||
private | |||
def find_project |
@@ -21,6 +21,9 @@ class IssueRelationsController < ApplicationController | |||
def new | |||
@relation = IssueRelation.new(params[:relation]) | |||
@relation.issue_from = @issue | |||
if params[:relation] && !params[:relation][:issue_to_id].blank? | |||
@relation.issue_to = Issue.visible.find_by_id(params[:relation][:issue_to_id]) | |||
end | |||
@relation.save if request.post? | |||
respond_to do |format| | |||
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } |
@@ -18,11 +18,11 @@ | |||
class IssuesController < ApplicationController | |||
menu_item :new_issue, :only => :new | |||
before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment] | |||
before_filter :find_issue, :only => [:show, :edit, :reply] | |||
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] | |||
before_filter :find_project, :only => [:new, :update_form, :preview, :gantt, :calendar] | |||
before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu] | |||
before_filter :find_optional_project, :only => [:index, :changes] | |||
before_filter :find_project, :only => [:new, :update_form, :preview] | |||
before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu] | |||
before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar] | |||
accept_key_auth :index, :changes | |||
helper :journals | |||
@@ -30,8 +30,6 @@ class IssuesController < ApplicationController | |||
include ProjectsHelper | |||
helper :custom_fields | |||
include CustomFieldsHelper | |||
helper :ifpdf | |||
include IfpdfHelper | |||
helper :issue_relations | |||
include IssueRelationsHelper | |||
helper :watchers | |||
@@ -43,11 +41,13 @@ class IssuesController < ApplicationController | |||
include SortHelper | |||
include IssuesHelper | |||
helper :timelog | |||
include Redmine::Export::PDF | |||
def index | |||
sort_init "#{Issue.table_name}.id", "desc" | |||
sort_update | |||
retrieve_query | |||
sort_init 'id', 'desc' | |||
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h})) | |||
if @query.valid? | |||
limit = per_page_option | |||
respond_to do |format| | |||
@@ -67,7 +67,7 @@ class IssuesController < ApplicationController | |||
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } | |||
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } | |||
format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') } | |||
format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') } | |||
format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') } | |||
end | |||
else | |||
# Send html if the query is not valid | |||
@@ -78,9 +78,10 @@ class IssuesController < ApplicationController | |||
end | |||
def changes | |||
sort_init "#{Issue.table_name}.id", "desc" | |||
sort_update | |||
retrieve_query | |||
sort_init 'id', 'desc' | |||
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h})) | |||
if @query.valid? | |||
@journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ], | |||
:conditions => @query.statement, | |||
@@ -104,7 +105,7 @@ class IssuesController < ApplicationController | |||
respond_to do |format| | |||
format.html { render :template => 'issues/show.rhtml' } | |||
format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' } | |||
format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } | |||
format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } | |||
end | |||
end | |||
@@ -121,7 +122,10 @@ class IssuesController < ApplicationController | |||
render :nothing => true, :layout => true | |||
return | |||
end | |||
@issue.attributes = params[:issue] | |||
if params[:issue].is_a?(Hash) | |||
@issue.attributes = params[:issue] | |||
@issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project) | |||
end | |||
@issue.author = User.current | |||
default_status = IssueStatus.default | |||
@@ -143,7 +147,9 @@ class IssuesController < ApplicationController | |||
attach_files(@issue, params[:attachments]) | |||
flash[:notice] = l(:notice_successful_create) | |||
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') | |||
redirect_to :controller => 'issues', :action => 'show', :id => @issue | |||
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) | |||
redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } : | |||
{ :action => 'show', :id => @issue }) | |||
return | |||
end | |||
end | |||
@@ -176,9 +182,12 @@ class IssuesController < ApplicationController | |||
@time_entry.attributes = params[:time_entry] | |||
attachments = attach_files(@issue, params[:attachments]) | |||
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} | |||
call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal}) | |||
if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save | |||
# Log spend time | |||
if current_role.allowed_to?(:log_time) | |||
if User.current.allowed_to?(:log_time, @project) | |||
@time_entry.save | |||
end | |||
if !journal.new_record? | |||
@@ -186,6 +195,7 @@ class IssuesController < ApplicationController | |||
flash[:notice] = l(:notice_successful_update) | |||
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') | |||
end | |||
call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal}) | |||
redirect_to(params[:back_to] || {:action => 'show', :id => @issue}) | |||
end | |||
end | |||
@@ -222,6 +232,7 @@ class IssuesController < ApplicationController | |||
assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id]) | |||
category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id]) | |||
fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id]) | |||
custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil | |||
unsaved_issue_ids = [] | |||
@issues.each do |issue| | |||
@@ -233,6 +244,7 @@ class IssuesController < ApplicationController | |||
issue.start_date = params[:start_date] unless params[:start_date].blank? | |||
issue.due_date = params[:due_date] unless params[:due_date].blank? | |||
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank? | |||
issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty? | |||
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) | |||
# Don't save any change to the issue if the user is not authorized to apply the requested status | |||
if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save | |||
@@ -253,7 +265,8 @@ class IssuesController < ApplicationController | |||
end | |||
# Find potential statuses the user could be allowed to switch issues to | |||
@available_statuses = Workflow.find(:all, :include => :new_status, | |||
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq | |||
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort | |||
@custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'} | |||
end | |||
def move | |||
@@ -261,7 +274,7 @@ class IssuesController < ApplicationController | |||
# find projects to which the user is allowed to move the issue | |||
if User.current.admin? | |||
# admin is allowed to move issues to any active (visible) project | |||
@allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name') | |||
@allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current)) | |||
else | |||
User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)} | |||
end | |||
@@ -273,7 +286,7 @@ class IssuesController < ApplicationController | |||
unsaved_issue_ids = [] | |||
@issues.each do |issue| | |||
issue.init_journal(User.current) | |||
unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) | |||
unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options]) | |||
end | |||
if unsaved_issue_ids.empty? | |||
flash[:notice] = l(:notice_successful_update) unless @issues.empty? | |||
@@ -310,17 +323,6 @@ class IssuesController < ApplicationController | |||
@issues.each(&:destroy) | |||
redirect_to :action => 'index', :project_id => @project | |||
end | |||
def destroy_attachment | |||
a = @issue.attachments.find(params[:attachment_id]) | |||
a.destroy | |||
journal = @issue.init_journal(User.current) | |||
journal.details << JournalDetail.new(:property => 'attachment', | |||
:prop_key => a.id, | |||
:old_value => a.filename) | |||
journal.save | |||
redirect_to :action => 'show', :id => @issue | |||
end | |||
def gantt | |||
@gantt = Redmine::Helpers::Gantt.new(params) | |||
@@ -348,8 +350,8 @@ class IssuesController < ApplicationController | |||
respond_to do |format| | |||
format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? } | |||
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image') | |||
format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-gantt.pdf") } | |||
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.png") } if @gantt.respond_to?('to_image') | |||
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") } | |||
end | |||
end | |||
@@ -450,9 +452,9 @@ private | |||
end | |||
def find_optional_project | |||
return true unless params[:project_id] | |||
@project = Project.find(params[:project_id]) | |||
authorize | |||
@project = Project.find(params[:project_id]) unless params[:project_id].blank? | |||
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true) | |||
allowed ? true : deny_access | |||
rescue ActiveRecord::RecordNotFound | |||
render_404 | |||
end |
@@ -22,6 +22,7 @@ class JournalsController < ApplicationController | |||
if request.post? | |||
@journal.update_attributes(:notes => params[:notes]) if params[:notes] | |||
@journal.destroy if @journal.details.empty? && @journal.notes.blank? | |||
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params}) | |||
respond_to do |format| | |||
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id } | |||
format.js { render :action => 'update' } |
@@ -19,7 +19,7 @@ class MessagesController < ApplicationController | |||
menu_item :boards | |||
before_filter :find_board, :only => [:new, :preview] | |||
before_filter :find_message, :except => [:new, :preview] | |||
before_filter :authorize, :except => :preview | |||
before_filter :authorize, :except => [:preview, :edit, :destroy] | |||
verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show } | |||
verify :xhr => true, :only => :quote | |||
@@ -30,7 +30,7 @@ class MessagesController < ApplicationController | |||
# Show a topic and its replies | |||
def show | |||
@replies = @topic.children | |||
@replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}]) | |||
@replies.reverse! if User.current.wants_comments_in_reverse_order? | |||
@reply = Message.new(:subject => "RE: #{@message.subject}") | |||
render :action => "show", :layout => false if request.xhr? | |||
@@ -65,7 +65,8 @@ class MessagesController < ApplicationController | |||
# Edit a message | |||
def edit | |||
if params[:message] && User.current.allowed_to?(:edit_messages, @project) | |||
render_403 and return false unless @message.editable_by?(User.current) | |||
if params[:message] | |||
@message.locked = params[:message]['locked'] | |||
@message.sticky = params[:message]['sticky'] | |||
end | |||
@@ -78,6 +79,7 @@ class MessagesController < ApplicationController | |||
# Delete a messages | |||
def destroy | |||
render_403 and return false unless @message.destroyable_by?(User.current) | |||
@message.destroy | |||
redirect_to @message.parent.nil? ? | |||
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } : |
@@ -19,6 +19,7 @@ class MyController < ApplicationController | |||
before_filter :require_login | |||
helper :issues | |||
helper :custom_fields | |||
BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues, | |||
'issuesreportedbyme' => :label_reported_issues, |
@@ -29,12 +29,16 @@ class ProjectsController < ApplicationController | |||
before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ] | |||
accept_key_auth :activity | |||
after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller| | |||
if controller.request.post? | |||
controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt' | |||
end | |||
end | |||
helper :sort | |||
include SortHelper | |||
helper :custom_fields | |||
include CustomFieldsHelper | |||
helper :ifpdf | |||
include IfpdfHelper | |||
helper :issues | |||
helper IssuesHelper | |||
helper :queries | |||
@@ -45,17 +49,14 @@ class ProjectsController < ApplicationController | |||
# Lists visible projects | |||
def index | |||
projects = Project.find :all, | |||
:conditions => Project.visible_by(User.current), | |||
:include => :parent | |||
respond_to do |format| | |||
format.html { | |||
@project_tree = projects.group_by {|p| p.parent || p} | |||
@project_tree.keys.each {|p| @project_tree[p] -= [p]} | |||
@projects = Project.visible.find(:all, :order => 'lft') | |||
} | |||
format.atom { | |||
render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i), | |||
:title => "#{Setting.app_title}: #{l(:label_project_latest)}") | |||
projects = Project.visible.find(:all, :order => 'created_on DESC', | |||
:limit => Setting.feeds_limit.to_i) | |||
render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}") | |||
} | |||
end | |||
end | |||
@@ -64,9 +65,6 @@ class ProjectsController < ApplicationController | |||
def add | |||
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") | |||
@trackers = Tracker.all | |||
@root_projects = Project.find(:all, | |||
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", | |||
:order => 'name') | |||
@project = Project.new(params[:project]) | |||
if request.get? | |||
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? | |||
@@ -76,6 +74,7 @@ class ProjectsController < ApplicationController | |||
else | |||
@project.enabled_module_names = params[:enabled_modules] | |||
if @project.save | |||
@project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id') | |||
flash[:notice] = l(:notice_successful_create) | |||
redirect_to :controller => 'admin', :action => 'projects' | |||
end | |||
@@ -84,20 +83,26 @@ class ProjectsController < ApplicationController | |||
# Show @project | |||
def show | |||
if params[:jump] | |||
# try to redirect to the requested menu item | |||
redirect_to_project_menu_item(@project, params[:jump]) && return | |||
end | |||
@members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} | |||
@subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current)) | |||
@subprojects = @project.children.visible | |||
@ancestors = @project.ancestors.visible | |||
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") | |||
@trackers = @project.rolled_up_trackers | |||
cond = @project.project_condition(Setting.display_subprojects_issues?) | |||
Issue.visible_by(User.current) do | |||
@open_issues_by_tracker = Issue.count(:group => :tracker, | |||
@open_issues_by_tracker = Issue.visible.count(:group => :tracker, | |||
:include => [:project, :status, :tracker], | |||
:conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false]) | |||
@total_issues_by_tracker = Issue.count(:group => :tracker, | |||
@total_issues_by_tracker = Issue.visible.count(:group => :tracker, | |||
:include => [:project, :status, :tracker], | |||
:conditions => cond) | |||
end | |||
TimeEntry.visible_by(User.current) do | |||
@total_hours = TimeEntry.sum(:hours, | |||
:include => :project, | |||
@@ -107,9 +112,6 @@ class ProjectsController < ApplicationController | |||
end | |||
def settings | |||
@root_projects = Project.find(:all, | |||
:conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id], | |||
:order => 'name') | |||
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") | |||
@issue_category ||= IssueCategory.new | |||
@member ||= @project.members.new | |||
@@ -123,6 +125,7 @@ class ProjectsController < ApplicationController | |||
if request.post? | |||
@project.attributes = params[:project] | |||
if @project.save | |||
@project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id') | |||
flash[:notice] = l(:notice_successful_update) | |||
redirect_to :action => 'settings', :id => @project | |||
else | |||
@@ -188,18 +191,26 @@ class ProjectsController < ApplicationController | |||
def add_file | |||
if request.post? | |||
@version = @project.versions.find_by_id(params[:version_id]) | |||
attachments = attach_files(@version, params[:attachments]) | |||
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added') | |||
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id])) | |||
attachments = attach_files(container, params[:attachments]) | |||
if !attachments.empty? && Setting.notified_events.include?('file_added') | |||
Mailer.deliver_attachments_added(attachments) | |||
end | |||
redirect_to :controller => 'projects', :action => 'list_files', :id => @project | |||
return | |||
end | |||
@versions = @project.versions.sort | |||
end | |||
def list_files | |||
sort_init "#{Attachment.table_name}.filename", "asc" | |||
sort_update | |||
@versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse | |||
sort_init 'filename', 'asc' | |||
sort_update 'filename' => "#{Attachment.table_name}.filename", | |||
'created_on' => "#{Attachment.table_name}.created_on", | |||
'size' => "#{Attachment.table_name}.filesize", | |||
'downloads' => "#{Attachment.table_name}.downloads" | |||
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)] | |||
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse | |||
render :layout => !request.xhr? | |||
end | |||
@@ -221,16 +232,19 @@ class ProjectsController < ApplicationController | |||
@days = Setting.activity_days_default.to_i | |||
if params[:from] | |||
begin; @date_to = params[:from].to_date; rescue; end | |||
begin; @date_to = params[:from].to_date + 1; rescue; end | |||
end | |||
@date_to ||= Date.today + 1 | |||
@date_from = @date_to - @days | |||
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') | |||
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id])) | |||
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects) | |||
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, | |||
:with_subprojects => @with_subprojects, | |||
:author => @author) | |||
@activity.scope_select {|t| !params["show_#{t}"].nil?} | |||
@activity.default_scope! if @activity.scope.empty? | |||
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty? | |||
events = @activity.events(@date_from, @date_to) | |||
@@ -240,10 +254,18 @@ class ProjectsController < ApplicationController | |||
render :layout => false if request.xhr? | |||
} | |||
format.atom { | |||
title = (@activity.scope.size == 1) ? l("label_#{@activity.scope.first.singularize}_plural") : l(:label_activity) | |||
title = l(:label_activity) | |||
if @author | |||
title = @author.name | |||
elsif @activity.scope.size == 1 | |||
title = l("label_#{@activity.scope.first.singularize}_plural") | |||
end | |||
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}") | |||
} | |||
end | |||
rescue ActiveRecord::RecordNotFound | |||
render_404 | |||
end | |||
private |
@@ -61,7 +61,7 @@ class ReportsController < ApplicationController | |||
render :template => "reports/issue_report_details" | |||
when "subproject" | |||
@field = "project_id" | |||
@rows = @project.active_children | |||
@rows = @project.descendants.active | |||
@data = issues_by_subproject | |||
@report_title = l(:field_subproject) | |||
render :template => "reports/issue_report_details" | |||
@@ -72,7 +72,7 @@ class ReportsController < ApplicationController | |||
@categories = @project.issue_categories | |||
@assignees = @project.members.collect { |m| m.user } | |||
@authors = @project.members.collect { |m| m.user } | |||
@subprojects = @project.active_children | |||
@subprojects = @project.descendants.active | |||
issues_by_tracker | |||
issues_by_version | |||
issues_by_priority | |||
@@ -229,8 +229,8 @@ private | |||
#{Issue.table_name} i, #{IssueStatus.table_name} s | |||
where | |||
i.status_id=s.id | |||
and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')}) | |||
group by s.id, s.is_closed, i.project_id") if @project.active_children.any? | |||
and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')}) | |||
group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any? | |||
@issues_by_subproject ||= [] | |||
end | |||
end |
@@ -44,6 +44,21 @@ class RepositoriesController < ApplicationController | |||
render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'} | |||
end | |||
def committers | |||
@committers = @repository.committers | |||
@users = @project.users | |||
additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id) | |||
@users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty? | |||
@users.compact! | |||
@users.sort! | |||
if request.post? && params[:committers].is_a?(Hash) | |||
# Build a hash with repository usernames as keys and corresponding user ids as values | |||
@repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h} | |||
flash[:notice] = l(:notice_successful_update) | |||
redirect_to :action => 'committers', :id => @project | |||
end | |||
end | |||
def destroy | |||
@repository.destroy | |||
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository' | |||
@@ -73,7 +88,7 @@ class RepositoriesController < ApplicationController | |||
def changes | |||
@entry = @repository.entry(@path, @rev) | |||
show_error_not_found and return unless @entry | |||
@changesets = @repository.changesets_for_path(@path) | |||
@changesets = @repository.changesets_for_path(@path, :limit => Setting.repository_log_display_limit.to_i) | |||
@properties = @repository.properties(@path, @rev) | |||
end | |||
@@ -84,7 +99,8 @@ class RepositoriesController < ApplicationController | |||
params['page'] | |||
@changesets = @repository.changesets.find(:all, | |||
:limit => @changeset_pages.items_per_page, | |||
:offset => @changeset_pages.current.offset) | |||
:offset => @changeset_pages.current.offset, | |||
:include => :user) | |||
respond_to do |format| | |||
format.html { render :layout => false if request.xhr? } | |||
@@ -111,6 +127,9 @@ class RepositoriesController < ApplicationController | |||
end | |||
def annotate | |||
@entry = @repository.entry(@path, @rev) | |||
show_error_not_found and return unless @entry | |||
@annotate = @repository.scm.annotate(@path, @rev) | |||
render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty? | |||
end |
@@ -79,27 +79,6 @@ class RolesController < ApplicationController | |||
redirect_to :action => 'list' | |||
end | |||
def workflow | |||
@role = Role.find_by_id(params[:role_id]) | |||
@tracker = Tracker.find_by_id(params[:tracker_id]) | |||
if request.post? | |||
Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) | |||
(params[:issue_status] || []).each { |old, news| | |||
news.each { |new| | |||
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new) | |||
} | |||
} | |||
if @role.save | |||
flash[:notice] = l(:notice_successful_update) | |||
redirect_to :action => 'workflow', :role_id => @role, :tracker_id => @tracker | |||
end | |||
end | |||
@roles = Role.find(:all, :order => 'builtin, position') | |||
@trackers = Tracker.find(:all, :order => 'position') | |||
@statuses = IssueStatus.find(:all, :order => 'position') | |||
end | |||
def report | |||
@roles = Role.find(:all, :order => 'builtin, position') | |||
@permissions = Redmine::AccessControl.permissions.select { |p| !p.public? } |
@@ -34,7 +34,7 @@ class SearchController < ApplicationController | |||
when 'my_projects' | |||
User.current.memberships.collect(&:project) | |||
when 'subprojects' | |||
@project ? ([ @project ] + @project.active_children) : nil | |||
@project ? (@project.self_and_descendants.active) : nil | |||
else | |||
@project | |||
end |
@@ -5,19 +5,19 @@ | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 2 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
class SettingsController < ApplicationController | |||
before_filter :require_admin | |||
def index | |||
edit | |||
render :action => 'edit' | |||
@@ -39,17 +39,21 @@ class SettingsController < ApplicationController | |||
@options = {} | |||
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] } | |||
@deliveries = ActionMailer::Base.perform_deliveries | |||
@guessed_host_and_path = request.host_with_port.dup | |||
@guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank? | |||
end | |||
def plugin | |||
plugin_id = params[:id].to_sym | |||
@plugin = Redmine::Plugin.registered_plugins[plugin_id] | |||
@plugin = Redmine::Plugin.find(params[:id]) | |||
if request.post? | |||
Setting["plugin_#{plugin_id}"] = params[:settings] | |||
Setting["plugin_#{@plugin.id}"] = params[:settings] | |||
flash[:notice] = l(:notice_successful_update) | |||
redirect_to :action => 'plugin', :id => params[:id] | |||
redirect_to :action => 'plugin', :id => @plugin.id | |||
end | |||
@partial = @plugin.settings[:partial] | |||
@settings = Setting["plugin_#{plugin_id}"] | |||
@settings = Setting["plugin_#{@plugin.id}"] | |||
rescue Redmine::PluginNotFound | |||
render_404 | |||
end | |||
end |
@@ -1,5 +1,5 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006-2007 Jean-Philippe Lang | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2009 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
@@ -16,31 +16,35 @@ | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
class SysController < ActionController::Base | |||
wsdl_service_name 'Sys' | |||
web_service_api SysApi | |||
web_service_scaffold :invoke | |||
before_filter :check_enabled | |||
before_invocation :check_enabled | |||
# Returns the projects list, with their repositories | |||
def projects_with_repository_enabled | |||
Project.has_module(:repository).find(:all, :include => :repository, :order => 'identifier') | |||
def projects | |||
p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => 'identifier') | |||
render :xml => p.to_xml(:include => :repository) | |||
end | |||
# Registers a repository for the given project identifier | |||
def repository_created(identifier, vendor, url) | |||
project = Project.find_by_identifier(identifier) | |||
# Do not create the repository if the project has already one | |||
return 0 unless project && project.repository.nil? | |||
logger.debug "Repository for #{project.name} was created" | |||
repository = Repository.factory(vendor, :project => project, :url => url) | |||
repository.save | |||
repository.id || 0 | |||
def create_project_repository | |||
project = Project.find(params[:id]) | |||
if project.repository | |||
render :nothing => true, :status => 409 | |||
else | |||
logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}." | |||
project.repository = Repository.factory(params[:vendor], params[:repository]) | |||
if project.repository && project.repository.save | |||
render :xml => project.repository, :status => 201 | |||
else | |||
render :nothing => true, :status => 422 | |||
end | |||
end | |||
end | |||
protected | |||
protected | |||
def check_enabled(name, args) | |||
Setting.sys_api_enabled? | |||
def check_enabled | |||
User.current = nil | |||
unless Setting.sys_api_enabled? | |||
render :nothing => 'Access denied. Repository management WS is disabled.', :status => 403 | |||
return false | |||
end | |||
end | |||
end |
@@ -138,7 +138,12 @@ class TimelogController < ApplicationController | |||
def details | |||
sort_init 'spent_on', 'desc' | |||
sort_update | |||
sort_update 'spent_on' => 'spent_on', | |||
'user' => 'user_id', | |||
'activity' => 'activity_id', | |||
'project' => "#{Project.table_name}.name", | |||
'issue' => 'issue_id', | |||
'hours' => 'hours' | |||
cond = ARCondition.new | |||
if @project.nil? |
@@ -40,8 +40,10 @@ class TrackersController < ApplicationController | |||
end | |||
flash[:notice] = l(:notice_successful_create) | |||
redirect_to :action => 'list' | |||
return | |||
end | |||
@trackers = Tracker.find :all, :order => 'position' | |||
@projects = Project.find(:all) | |||
end | |||
def edit | |||
@@ -49,7 +51,9 @@ class TrackersController < ApplicationController | |||
if request.post? and @tracker.update_attributes(params[:tracker]) | |||
flash[:notice] = l(:notice_successful_update) | |||
redirect_to :action => 'list' | |||
return | |||
end | |||
@projects = Project.find(:all) | |||
end | |||
def move |
@@ -30,18 +30,22 @@ class UsersController < ApplicationController | |||
def list | |||
sort_init 'login', 'asc' | |||
sort_update | |||
sort_update %w(login firstname lastname mail admin created_on last_login_on) | |||
@status = params[:status] ? params[:status].to_i : 1 | |||
conditions = "status <> 0" | |||
conditions = ["status=?", @status] unless @status == 0 | |||
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) | |||
unless params[:name].blank? | |||
name = "%#{params[:name].strip.downcase}%" | |||
c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name] | |||
end | |||
@user_count = User.count(:conditions => conditions) | |||
@user_count = User.count(:conditions => c.conditions) | |||
@user_pages = Paginator.new self, @user_count, | |||
per_page_option, | |||
params['page'] | |||
@users = User.find :all,:order => sort_clause, | |||
:conditions => conditions, | |||
:conditions => c.conditions, | |||
:limit => @user_pages.items_per_page, | |||
:offset => @user_pages.current.offset | |||
@@ -79,7 +83,7 @@ class UsersController < ApplicationController | |||
end | |||
@auth_sources = AuthSource.find(:all) | |||
@roles = Role.find_all_givable | |||
@projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects | |||
@projects = Project.active.find(:all, :order => 'lft') | |||
@membership ||= Member.new | |||
@memberships = @user.memberships | |||
end |
@@ -37,12 +37,6 @@ class VersionsController < ApplicationController | |||
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project | |||
end | |||
def destroy_file | |||
@version.attachments.find(params[:attachment_id]).destroy | |||
flash[:notice] = l(:notice_successful_delete) | |||
redirect_to :controller => 'projects', :action => 'list_files', :id => @project | |||
end | |||
def status_by | |||
respond_to do |format| | |||
format.html { render :action => 'show' } |
@@ -16,9 +16,15 @@ | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
class WelcomeController < ApplicationController | |||
caches_action :robots | |||
def index | |||
@news = News.latest User.current | |||
@projects = Project.latest User.current | |||
end | |||
def robots | |||
@projects = Project.public.active | |||
render :layout => false, :content_type => 'text/plain' | |||
end | |||
end |
@@ -19,8 +19,9 @@ require 'diff' | |||
class WikiController < ApplicationController | |||
before_filter :find_wiki, :authorize | |||
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy] | |||
verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index } | |||
verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index } | |||
helper :attachments | |||
include AttachmentsHelper | |||
@@ -44,11 +45,11 @@ class WikiController < ApplicationController | |||
return | |||
end | |||
@content = @page.content_for_version(params[:version]) | |||
if params[:export] == 'html' | |||
if params[:format] == 'html' | |||
export = render_to_string :action => 'export', :layout => false | |||
send_data(export, :type => 'text/html', :filename => "#{@page.title}.html") | |||
return | |||
elsif params[:export] == 'txt' | |||
elsif params[:format] == 'txt' | |||
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") | |||
return | |||
end | |||
@@ -63,7 +64,7 @@ class WikiController < ApplicationController | |||
@page.content = WikiContent.new(:page => @page) if @page.new_record? | |||
@content = @page.content_for_version(params[:version]) | |||
@content.text = "h1. #{@page.pretty_title}" if @content.text.blank? | |||
@content.text = initial_page_content(@page) if @content.text.blank? | |||
# don't keep previous comment | |||
@content.comments = nil | |||
if request.get? | |||
@@ -91,8 +92,7 @@ class WikiController < ApplicationController | |||
# rename a page | |||
def rename | |||
@page = @wiki.find_page(params[:page]) | |||
return render_403 unless editable? | |||
return render_403 unless editable? | |||
@page.redirect_existing_links = true | |||
# used to display the *original* title if some AR validation errors occur | |||
@original_title = @page.pretty_title | |||
@@ -103,15 +103,12 @@ class WikiController < ApplicationController | |||
end | |||
def protect | |||
page = @wiki.find_page(params[:page]) | |||
page.update_attribute :protected, params[:protected] | |||
redirect_to :action => 'index', :id => @project, :page => page.title | |||
@page.update_attribute :protected, params[:protected] | |||
redirect_to :action => 'index', :id => @project, :page => @page.title | |||
end | |||
# show page history | |||
def history | |||
@page = @wiki.find_page(params[:page]) | |||
@version_count = @page.content.versions.count | |||
@version_pages = Paginator.new self, @version_count, per_page_option, params['p'] | |||
# don't load text | |||
@@ -125,21 +122,19 @@ class WikiController < ApplicationController | |||
end | |||
def diff | |||
@page = @wiki.find_page(params[:page]) | |||
@diff = @page.diff(params[:version], params[:version_from]) | |||
render_404 unless @diff | |||
end | |||
def annotate | |||
@page = @wiki.find_page(params[:page]) | |||
@annotate = @page.annotate(params[:version]) | |||
render_404 unless @annotate | |||
end | |||
# remove a wiki page and its history | |||
def destroy | |||
@page = @wiki.find_page(params[:page]) | |||
return render_403 unless editable? | |||
@page.destroy if @page | |||
return render_403 unless editable? | |||
@page.destroy | |||
redirect_to :action => 'special', :id => @project, :page => 'Page_index' | |||
end | |||
@@ -181,19 +176,11 @@ class WikiController < ApplicationController | |||
end | |||
def add_attachment | |||
@page = @wiki.find_page(params[:page]) | |||
return render_403 unless editable? | |||
attach_files(@page, params[:attachments]) | |||
redirect_to :action => 'index', :page => @page.title | |||
end | |||
def destroy_attachment | |||
@page = @wiki.find_page(params[:page]) | |||
return render_403 unless editable? | |||
@page.attachments.find(params[:attachment_id]).destroy | |||
redirect_to :action => 'index', :page => @page.title | |||
end | |||
private | |||
def find_wiki | |||
@@ -204,8 +191,21 @@ private | |||
render_404 | |||
end | |||
# Finds the requested page and returns a 404 error if it doesn't exist | |||
def find_existing_page | |||
@page = @wiki.find_page(params[:page]) | |||
render_404 if @page.nil? | |||
end | |||
# Returns true if the current user is allowed to edit the page, otherwise false | |||
def editable?(page = @page) | |||
page.editable_by?(User.current) | |||
end | |||
# Returns the default content of a new wiki page | |||
def initial_page_content(page) | |||
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) | |||
extend helper unless self.instance_of?(helper) | |||
helper.instance_method(:initial_page_content).bind(self).call(page) | |||
end | |||
end |
@@ -0,0 +1,45 @@ | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2008 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 2 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
class WorkflowsController < ApplicationController | |||
before_filter :require_admin | |||
def index | |||
@workflow_counts = Workflow.count_by_tracker_and_role | |||
end | |||
def edit | |||
@role = Role.find_by_id(params[:role_id]) | |||
@tracker = Tracker.find_by_id(params[:tracker_id]) | |||
if request.post? | |||
Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) | |||
(params[:issue_status] || []).each { |old, news| | |||
news.each { |new| | |||
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new) | |||
} | |||
} | |||
if @role.save | |||
flash[:notice] = l(:notice_successful_update) | |||
redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker | |||
end | |||
end | |||
@roles = Role.find(:all, :order => 'builtin, position') | |||
@trackers = Tracker.find(:all, :order => 'position') | |||
@statuses = IssueStatus.find(:all, :order => 'position') | |||
end | |||
end |
@@ -17,7 +17,15 @@ | |||
module AdminHelper | |||
def project_status_options_for_select(selected) | |||
options_for_select([[l(:label_all), "*"], | |||
options_for_select([[l(:label_all), ''], | |||
[l(:status_active), 1]], selected) | |||
end | |||
def css_project_classes(project) | |||
s = 'project' | |||
s << ' root' if project.root? | |||
s << ' child' if project.child? | |||
s << (project.leaf? ? ' leaf' : ' parent') | |||
s | |||
end | |||
end |
@@ -5,26 +5,32 @@ | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 2 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
require 'coderay' | |||
require 'coderay/helpers/file_type' | |||
require 'forwardable' | |||
require 'cgi' | |||
module ApplicationHelper | |||
include Redmine::WikiFormatting::Macros::Definitions | |||
include GravatarHelper::PublicMethods | |||
extend Forwardable | |||
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter | |||
def current_role | |||
@current_role ||= User.current.role_for_project(@project) | |||
end | |||
# Return true if user is authorized for controller/action, otherwise false | |||
def authorize_for(controller, action) | |||
User.current.allowed_to?({:controller => controller, :action => action}, @project) | |||
@@ -34,7 +40,7 @@ module ApplicationHelper | |||
def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) | |||
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) | |||
end | |||
# Display a link to remote if user is authorized | |||
def link_to_remote_if_authorized(name, options = {}, html_options = nil) | |||
url = options[:url] || {} | |||
@@ -42,17 +48,17 @@ module ApplicationHelper | |||
end | |||
# Display a link to user's account page | |||
def link_to_user(user) | |||
user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous' | |||
def link_to_user(user, options={}) | |||
(user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous' | |||
end | |||
def link_to_issue(issue, options={}) | |||
options[:class] ||= '' | |||
options[:class] << ' issue' | |||
options[:class] << ' closed' if issue.closed? | |||
link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options | |||
end | |||
# Generates a link to an attachment. | |||
# Options: | |||
# * :text - Link text (default to attachment filename) | |||
@@ -60,37 +66,37 @@ module ApplicationHelper | |||
def link_to_attachment(attachment, options={}) | |||
text = options.delete(:text) || attachment.filename | |||
action = options.delete(:download) ? 'download' : 'show' | |||
link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options) | |||
end | |||
def toggle_link(name, id, options={}) | |||
onclick = "Element.toggle('#{id}'); " | |||
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") | |||
onclick << "return false;" | |||
link_to(name, "#", :onclick => onclick) | |||
end | |||
def image_to_function(name, function, html_options = {}) | |||
html_options.symbolize_keys! | |||
tag(:input, html_options.merge({ | |||
:type => "image", :src => image_path(name), | |||
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" | |||
tag(:input, html_options.merge({ | |||
:type => "image", :src => image_path(name), | |||
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" | |||
})) | |||
end | |||
def prompt_to_remote(name, text, param, url, html_options = {}) | |||
html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;" | |||
link_to name, {}, html_options | |||
end | |||
def format_date(date) | |||
return nil unless date | |||
# "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed) | |||
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) | |||
date.strftime(@date_format) | |||
end | |||
def format_time(time, include_date = true) | |||
return nil unless time | |||
time = time.to_time if time.is_a?(String) | |||
@@ -100,43 +106,147 @@ module ApplicationHelper | |||
@time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format) | |||
include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format) | |||
end | |||
def format_activity_title(text) | |||
h(truncate_single_line(text, 100)) | |||
end | |||
def format_activity_day(date) | |||
date == Date.today ? l(:label_today).titleize : format_date(date) | |||
end | |||
def format_activity_description(text) | |||
h(truncate(text.to_s, 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />") | |||
end | |||
def distance_of_date_in_words(from_date, to_date = 0) | |||
from_date = from_date.to_date if from_date.respond_to?(:to_date) | |||
to_date = to_date.to_date if to_date.respond_to?(:to_date) | |||
distance_in_days = (to_date - from_date).abs | |||
lwr(:actionview_datehelper_time_in_words_day, distance_in_days) | |||
end | |||
def due_date_distance_in_words(date) | |||
if date | |||
l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) | |||
end | |||
end | |||
def render_page_hierarchy(pages, node=nil) | |||
content = '' | |||
if pages[node] | |||
content << "<ul class=\"pages-hierarchy\">\n" | |||
pages[node].each do |page| | |||
content << "<li>" | |||
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title}, | |||
:title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) | |||
content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id] | |||
content << "</li>\n" | |||
end | |||
content << "</ul>\n" | |||
end | |||
content | |||
end | |||
# Renders flash messages | |||
def render_flash_messages | |||
s = '' | |||
flash.each do |k,v| | |||
s << content_tag('div', v, :class => "flash #{k}") | |||
end | |||
s | |||
end | |||
# Renders the project quick-jump box | |||
def render_project_jump_box | |||
# Retrieve them now to avoid a COUNT query | |||
projects = User.current.projects.all | |||
if projects.any? | |||
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' + | |||
"<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" + | |||
'<option disabled="disabled">---</option>' | |||
s << project_tree_options_for_select(projects) do |p| | |||
{ :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) } | |||
end | |||
s << '</select>' | |||
s | |||
end | |||
end | |||
def project_tree_options_for_select(projects, options = {}) | |||
s = '' | |||
project_tree(projects) do |project, level| | |||
name_prefix = (level > 0 ? (' ' * 2 * level + '» ') : '') | |||
tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)} | |||
tag_options.merge!(yield(project)) if block_given? | |||
s << content_tag('option', name_prefix + h(project), tag_options) | |||
end | |||
s | |||
end | |||
# Yields the given block for each project with its level in the tree | |||
def project_tree(projects, &block) | |||
ancestors = [] | |||
projects.sort_by(&:lft).each do |project| | |||
while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) | |||
ancestors.pop | |||
end | |||
yield project, ancestors.size | |||
ancestors << project | |||
end | |||
end | |||
def project_nested_ul(projects, &block) | |||
s = '' | |||
if projects.any? | |||
ancestors = [] | |||
projects.sort_by(&:lft).each do |project| | |||
if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) | |||
s << "<ul>\n" | |||
else | |||
ancestors.pop | |||
s << "</li>" | |||
while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) | |||
ancestors.pop | |||
s << "</ul></li>\n" | |||
end | |||
end | |||
s << "<li>" | |||
s << yield(project).to_s | |||
ancestors << project | |||
end | |||
s << ("</li></ul>\n" * ancestors.size) | |||
end | |||
s | |||
end | |||
# Truncates and returns the string as a single line | |||
def truncate_single_line(string, *args) | |||
truncate(string, *args).gsub(%r{[\r\n]+}m, ' ') | |||
end | |||
def html_hours(text) | |||
text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>') | |||
end | |||
def authoring(created, author) | |||
time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) | |||
def authoring(created, author, options={}) | |||
time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) : | |||
link_to(distance_of_time_in_words(Time.now, created), | |||
{:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date}, | |||
:title => format_time(created)) | |||
author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous') | |||
l(:label_added_time_by, author_tag, time_tag) | |||
l(options[:label] || :label_added_time_by, author_tag, time_tag) | |||
end | |||
def l_or_humanize(s) | |||
l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize | |||
def l_or_humanize(s, options={}) | |||
k = "#{options[:prefix]}#{s}".to_sym | |||
l_has_string?(k) ? l(k) : s.to_s.humanize | |||
end | |||
def day_name(day) | |||
l(:general_day_names).split(',')[day-1] | |||
end | |||
def month_name(month) | |||
l(:actionview_datehelper_select_month_names).split(',')[month-1] | |||
end | |||
@@ -145,7 +255,7 @@ module ApplicationHelper | |||
type = CodeRay::FileType[name] | |||
type ? CodeRay.scan(content, type).html : h(content) | |||
end | |||
def to_path_param(path) | |||
path.to_s.split(%r{[/\\]}).select {|p| !p.blank?} | |||
end | |||
@@ -153,53 +263,56 @@ module ApplicationHelper | |||
def pagination_links_full(paginator, count=nil, options={}) | |||
page_param = options.delete(:page_param) || :page | |||
url_param = params.dup | |||
# don't reuse params if filters are present | |||
url_param.clear if url_param.has_key?(:set_filter) | |||
html = '' | |||
html << link_to_remote(('« ' + l(:label_previous)), | |||
{:update => 'content', | |||
:url => url_param.merge(page_param => paginator.current.previous), | |||
:complete => 'window.scrollTo(0,0)'}, | |||
{:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous | |||
# don't reuse query params if filters are present | |||
url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter) | |||
html = '' | |||
if paginator.current.previous | |||
html << link_to_remote_content_update('« ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' ' | |||
end | |||
html << (pagination_links_each(paginator, options) do |n| | |||
link_to_remote(n.to_s, | |||
{:url => {:params => url_param.merge(page_param => n)}, | |||
:update => 'content', | |||
:complete => 'window.scrollTo(0,0)'}, | |||
{:href => url_for(:params => url_param.merge(page_param => n))}) | |||
link_to_remote_content_update(n.to_s, url_param.merge(page_param => n)) | |||
end || '') | |||
html << ' ' + link_to_remote((l(:label_next) + ' »'), | |||
{:update => 'content', | |||
:url => url_param.merge(page_param => paginator.current.next), | |||
:complete => 'window.scrollTo(0,0)'}, | |||
{:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next | |||
if paginator.current.next | |||
html << ' ' + link_to_remote_content_update((l(:label_next) + ' »'), url_param.merge(page_param => paginator.current.next)) | |||
end | |||
unless count.nil? | |||
html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ') | |||
html << [ | |||
" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", | |||
per_page_links(paginator.items_per_page) | |||
].compact.join(' | ') | |||
end | |||
html | |||
html | |||
end | |||
def per_page_links(selected=nil) | |||
url_param = params.dup | |||
url_param.clear if url_param.has_key?(:set_filter) | |||
links = Setting.per_page_options_array.collect do |n| | |||
n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)}, | |||
n == selected ? n : link_to_remote(n, {:update => "content", | |||
:url => params.dup.merge(:per_page => n), | |||
:method => :get}, | |||
{:href => url_for(url_param.merge(:per_page => n))}) | |||
end | |||
links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil | |||
end | |||
def breadcrumb(*args) | |||
elements = args.flatten | |||
elements.any? ? content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') : nil | |||
end | |||
def other_formats_links(&block) | |||
concat('<p class="other-formats">' + l(:label_export_to), block.binding) | |||
yield Redmine::Views::OtherFormatsBuilder.new(self) | |||
concat('</p>', block.binding) | |||
end | |||
def html_title(*args) | |||
if args.empty? | |||
title = [] | |||
@@ -234,32 +347,30 @@ module ApplicationHelper | |||
raise ArgumentError, 'invalid arguments to textilizable' | |||
end | |||
return '' if text.blank? | |||
only_path = options.delete(:only_path) == false ? false : true | |||
# when using an image link, try to use an attachment, if possible | |||
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) | |||
if attachments | |||
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m| | |||
attachments = attachments.sort_by(&:created_on).reverse | |||
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m| | |||
style = $1 | |||
filename = $6 | |||
rf = Regexp.new(filename, Regexp::IGNORECASE) | |||
filename = $6.downcase | |||
# search for the picture in attachments | |||
if found = attachments.detect { |att| att.filename =~ rf } | |||
if found = attachments.detect { |att| att.filename.downcase == filename } | |||
image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found | |||
desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1") | |||
alt = desc.blank? ? nil : "(#{desc})" | |||
"!#{style}#{image_url}#{alt}!" | |||
else | |||
"!#{style}#{filename}!" | |||
m | |||
end | |||
end | |||
end | |||
text = (Setting.text_formatting == 'textile') ? | |||
Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } : | |||
simple_format(auto_link(h(text))) | |||
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) } | |||
# different methods for formatting wiki links | |||
case options[:wiki_links] | |||
@@ -272,11 +383,11 @@ module ApplicationHelper | |||
else | |||
format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) } | |||
end | |||
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) | |||
# Wiki links | |||
# | |||
# | |||
# Examples: | |||
# [[mypage]] | |||
# [[mypage|mytext]] | |||
@@ -294,7 +405,7 @@ module ApplicationHelper | |||
page = $2 | |||
title ||= $1 if page.blank? | |||
end | |||
if link_project && link_project.wiki | |||
# extract anchor | |||
anchor = nil | |||
@@ -307,7 +418,7 @@ module ApplicationHelper | |||
:class => ('wiki-page' + (wiki_page ? '' : ' new'))) | |||
else | |||
# project or wiki doesn't exist | |||
title || page | |||
all | |||
end | |||
else | |||
all | |||
@@ -315,7 +426,7 @@ module ApplicationHelper | |||
end | |||
# Redmine links | |||
# | |||
# | |||
# Examples: | |||
# Issues: | |||
# #52 -> Link to issue #52 | |||
@@ -354,7 +465,7 @@ module ApplicationHelper | |||
oid = oid.to_i | |||
case prefix | |||
when nil | |||
if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current)) | |||
if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current)) | |||
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid}, | |||
:class => (issue.closed? ? 'issue closed' : 'issue'), | |||
:title => "#{truncate(issue.subject, 100)} (#{issue.status.name})") | |||
@@ -422,10 +533,10 @@ module ApplicationHelper | |||
end | |||
leading + (link || "#{prefix}#{sep}#{oid}") | |||
end | |||
text | |||
end | |||
# Same as Rails' simple_format helper without using paragraphs | |||
def simple_format_without_paragraph(text) | |||
text.to_s. | |||
@@ -433,7 +544,7 @@ module ApplicationHelper | |||
gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br | |||
gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br | |||
end | |||
def error_messages_for(object_name, options = {}) | |||
options = options.symbolize_keys | |||
object = instance_variable_get("@#{object_name}") | |||
@@ -451,14 +562,14 @@ module ApplicationHelper | |||
end | |||
# retrieve custom values error messages | |||
if object.errors[:custom_values] | |||
object.custom_values.each do |v| | |||
object.custom_values.each do |v| | |||
v.errors.each do |attr, msg| | |||
next if msg.nil? | |||
msg = msg.first if msg.is_a? Array | |||
full_messages << "« " + v.custom_field.name + " » " + l(msg) | |||
end | |||
end | |||
end | |||
end | |||
content_tag("div", | |||
content_tag( | |||
options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":" | |||
@@ -470,34 +581,35 @@ module ApplicationHelper | |||
"" | |||
end | |||
end | |||
def lang_options_for_select(blank=true) | |||
(blank ? [["(auto)", ""]] : []) + | |||
(blank ? [["(auto)", ""]] : []) + | |||
GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } | |||
end | |||
def label_tag_for(name, option_tags = nil, options = {}) | |||
label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") | |||
content_tag("label", label_text) | |||
end | |||
def labelled_tabular_form_for(name, object, options, &proc) | |||
options[:html] ||= {} | |||
options[:html][:class] = 'tabular' unless options[:html].has_key?(:class) | |||
form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc) | |||
end | |||
def back_url_hidden_field_tag | |||
back_url = params[:back_url] || request.env['HTTP_REFERER'] | |||
hidden_field_tag('back_url', back_url) unless back_url.blank? | |||
back_url = CGI.unescape(back_url.to_s) | |||
hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank? | |||
end | |||
def check_all_links(form_name) | |||
link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + | |||
" | " + | |||
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") | |||
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") | |||
end | |||
def progress_bar(pcts, options={}) | |||
pcts = [pcts, pcts] unless pcts.is_a?(Array) | |||
pcts[1] = pcts[1] - pcts[0] | |||
@@ -506,13 +618,13 @@ module ApplicationHelper | |||
legend = options[:legend] || '' | |||
content_tag('table', | |||
content_tag('tr', | |||
(pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') + | |||
(pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') + | |||
(pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '') | |||
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') + | |||
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') + | |||
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '') | |||
), :class => 'progress', :style => "width: #{width};") + | |||
content_tag('p', legend, :class => 'pourcent') | |||
end | |||
def context_menu_link(name, url, options={}) | |||
options[:class] ||= '' | |||
if options.delete(:selected) | |||
@@ -528,7 +640,7 @@ module ApplicationHelper | |||
end | |||
link_to name, url, options | |||
end | |||
def calendar_for(field_id) | |||
include_calendar_headers_tags | |||
image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) + | |||
@@ -546,26 +658,44 @@ module ApplicationHelper | |||
end | |||
end | |||
end | |||
def wikitoolbar_for(field_id) | |||
return '' unless Setting.text_formatting == 'textile' | |||
help_link = l(:setting_text_formatting) + ': ' + | |||
link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'), | |||
:onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;") | |||
javascript_include_tag('jstoolbar/jstoolbar') + | |||
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") + | |||
javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();") | |||
end | |||
def content_for(name, content = nil, &block) | |||
@has_content ||= {} | |||
@has_content[name] = true | |||
super(name, content, &block) | |||
end | |||
def has_content?(name) | |||
(@has_content && @has_content[name]) || false | |||
end | |||
# Returns the avatar image tag for the given +user+ if avatars are enabled | |||
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') | |||
def avatar(user, options = { }) | |||
if Setting.gravatar_enabled? | |||
email = nil | |||
if user.respond_to?(:mail) | |||
email = user.mail | |||
elsif user.to_s =~ %r{<(.+?)>} | |||
email = $1 | |||
end | |||
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil | |||
end | |||
end | |||
private | |||
def wiki_helper | |||
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) | |||
extend helper | |||
return self | |||
end | |||
def link_to_remote_content_update(text, url_params) | |||
link_to_remote(text, | |||
{:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'}, | |||
{:href => url_for(:params => url_params)} | |||
) | |||
end | |||
end |
@@ -16,10 +16,15 @@ | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
module AttachmentsHelper | |||
# displays the links to a collection of attachments | |||
def link_to_attachments(attachments, options = {}) | |||
if attachments.any? | |||
render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options} | |||
# Displays view/delete links to the attachments of the given object | |||
# Options: | |||
# :author -- author names are not displayed if set to false | |||
def link_to_attachments(container, options = {}) | |||
options.assert_valid_keys(:author) | |||
if container.attachments.any? | |||
options = {:deletable => container.attachments_deletable?, :author => true}.merge(options) | |||
render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options} | |||
end | |||
end | |||
@@ -1,85 +0,0 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 2 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
require 'iconv' | |||
require 'rfpdf/chinese' | |||
module IfpdfHelper | |||
class IFPDF < FPDF | |||
include GLoc | |||
attr_accessor :footer_date | |||
def initialize(lang) | |||
super() | |||
set_language_if_valid lang | |||
case current_language.to_s | |||
when 'ja' | |||
extend(PDF_Japanese) | |||
AddSJISFont() | |||
@font_for_content = 'SJIS' | |||
@font_for_footer = 'SJIS' | |||
when 'zh' | |||
extend(PDF_Chinese) | |||
AddGBFont() | |||
@font_for_content = 'GB' | |||
@font_for_footer = 'GB' | |||
when 'zh-tw' | |||
extend(PDF_Chinese) | |||
AddBig5Font() | |||
@font_for_content = 'Big5' | |||
@font_for_footer = 'Big5' | |||
else | |||
@font_for_content = 'Arial' | |||
@font_for_footer = 'Helvetica' | |||
end | |||
SetCreator("redMine #{Redmine::VERSION}") | |||
SetFont(@font_for_content) | |||
end | |||
def SetFontStyle(style, size) | |||
SetFont(@font_for_content, style, size) | |||
end | |||
def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='') | |||
@ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8') | |||
# these quotation marks are not correctly rendered in the pdf | |||
txt = txt.gsub(/[“”]/, '"') if txt | |||
txt = begin | |||
# 0x5c char handling | |||
txtar = txt.split('\\') | |||
txtar << '' if txt[-1] == ?\\ | |||
txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\") | |||
rescue | |||
txt | |||
end || '' | |||
super w,h,txt,border,ln,align,fill,link | |||
end | |||
def Footer | |||
SetFont(@font_for_footer, 'I', 8) | |||
SetY(-15) | |||
SetX(15) | |||
Cell(0, 5, @footer_date, 0, 0, 'L') | |||
SetY(-15) | |||
SetX(-30) | |||
Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C') | |||
end | |||
end | |||
end |
@@ -33,6 +33,14 @@ module IssuesHelper | |||
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}" | |||
end | |||
# Returns a string of css classes that apply to the given issue | |||
def css_issue_classes(issue) | |||
s = "issue status-#{issue.status.position} priority-#{issue.priority.position}" | |||
s << ' closed' if issue.closed? | |||
s << ' overdue' if issue.overdue? | |||
s | |||
end | |||
def sidebar_queries | |||
unless @sidebar_queries | |||
# User can see public queries and his own queries |
@@ -21,18 +21,6 @@ module ProjectsHelper | |||
link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options | |||
end | |||
def format_activity_title(text) | |||
h(truncate_single_line(text, 100)) | |||
end | |||
def format_activity_day(date) | |||
date == Date.today ? l(:label_today).titleize : format_date(date) | |||
end | |||
def format_activity_description(text) | |||
h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...')) | |||
end | |||
def project_settings_tabs | |||
tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural}, | |||
{:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural}, | |||
@@ -45,4 +33,39 @@ module ProjectsHelper | |||
] | |||
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)} | |||
end | |||
def parent_project_select_tag(project) | |||
options = '<option></option>' + project_tree_options_for_select(project.possible_parents, :selected => project.parent) | |||
content_tag('select', options, :name => 'project[parent_id]') | |||
end | |||
# Renders a tree of projects as a nested set of unordered lists | |||
# The given collection may be a subset of the whole project tree | |||
# (eg. some intermediate nodes are private and can not be seen) | |||
def render_project_hierarchy(projects) | |||
s = '' | |||
if projects.any? | |||
ancestors = [] | |||
projects.each do |project| | |||
if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) | |||
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n" | |||
else | |||
ancestors.pop | |||
s << "</li>" | |||
while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) | |||
ancestors.pop | |||
s << "</ul></li>\n" | |||
end | |||
end | |||
classes = (ancestors.empty? ? 'root' : 'child') | |||
s << "<li class='#{classes}'><div class='#{classes}'>" + | |||
link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}") | |||
s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank? | |||
s << "</div>\n" | |||
ancestors << project | |||
end | |||
s << ("</li></ul>\n" * ancestors.size) | |||
end | |||
s | |||
end | |||
end |
@@ -22,8 +22,8 @@ module QueriesHelper | |||
end | |||
def column_header(column) | |||
column.sortable ? sort_header_tag(column.sortable, :caption => column.caption, | |||
:default_order => column.default_order) : | |||
column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption, | |||
:default_order => column.default_order) : | |||
content_tag('th', column.caption) | |||
end | |||
@@ -44,6 +44,8 @@ module QueriesHelper | |||
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) | |||
when :done_ratio | |||
progress_bar(value, :width => '80px') | |||
when :fixed_version | |||
link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id }) | |||
else | |||
h(value) | |||
end |
@@ -22,6 +22,12 @@ module RepositoriesHelper | |||
txt.to_s[0,8] | |||
end | |||
def truncate_at_line_break(text, length = 255) | |||
if text | |||
text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...') | |||
end | |||
end | |||
def render_properties(properties) | |||
unless properties.nil? || properties.empty? | |||
content = '' |
@@ -44,7 +44,7 @@ module SearchHelper | |||
def project_select_tag | |||
options = [[l(:label_project_all), 'all']] | |||
options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty? | |||
options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.active_children.empty? | |||
options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty? | |||
options << [@project.name, ''] unless @project.nil? | |||
select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1 | |||
end |
@@ -18,6 +18,7 @@ | |||
module SettingsHelper | |||
def administration_settings_tabs | |||
tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general}, | |||
{:name => 'display', :partial => 'settings/display', :label => :label_display}, | |||
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication}, | |||
{:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural}, | |||
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking}, |
@@ -67,23 +67,31 @@ module SortHelper | |||
# Updates the sort state. Call this in the controller prior to calling | |||
# sort_clause. | |||
# | |||
def sort_update() | |||
if params[:sort_key] | |||
sort = {:key => params[:sort_key], :order => params[:sort_order]} | |||
# sort_keys can be either an array or a hash of allowed keys | |||
def sort_update(sort_keys) | |||
sort_key = params[:sort_key] | |||
sort_key = nil unless (sort_keys.is_a?(Array) ? sort_keys.include?(sort_key) : sort_keys[sort_key]) | |||
sort_order = (params[:sort_order] == 'desc' ? 'DESC' : 'ASC') | |||
if sort_key | |||
sort = {:key => sort_key, :order => sort_order} | |||
elsif session[@sort_name] | |||
sort = session[@sort_name] # Previous sort. | |||
else | |||
sort = @sort_default | |||
end | |||
session[@sort_name] = sort | |||
sort_column = (sort_keys.is_a?(Hash) ? sort_keys[sort[:key]] : sort[:key]) | |||
@sort_clause = (sort_column.blank? ? nil : [sort_column].flatten.collect {|s| "#{s} #{sort[:order]}"}.join(',')) | |||
end | |||
# Returns an SQL sort clause corresponding to the current sort state. | |||
# Use this to sort the controller's table items collection. | |||
# | |||
def sort_clause() | |||
session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC') | |||
@sort_clause | |||
end | |||
# Returns a link which sorts by the named column. | |||
@@ -112,8 +120,11 @@ module SortHelper | |||
# don't reuse params if filters are present | |||
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options) | |||
# Add project_id to url_options | |||
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id) | |||
link_to_remote(caption, | |||
{:update => "content", :url => url_options}, | |||
{:update => "content", :url => url_options, :method => :get}, | |||
{:href => url_for(url_options)}) + | |||
(icon ? nbsp(2) + image_tag(icon) : '') | |||
end |
@@ -16,6 +16,8 @@ | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
module TimelogHelper | |||
include ApplicationHelper | |||
def render_timelog_breadcrumb | |||
links = [] | |||
links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil}) | |||
@@ -81,7 +83,7 @@ module TimelogHelper | |||
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } | |||
# csv lines | |||
entries.each do |entry| | |||
fields = [l_date(entry.spent_on), | |||
fields = [format_date(entry.spent_on), | |||
entry.user, | |||
entry.activity, | |||
entry.project, |
@@ -25,15 +25,10 @@ module UsersHelper | |||
end | |||
# Options for the new membership projects combo-box | |||
def projects_options_for_select(projects) | |||
def options_for_membership_project_select(user, projects) | |||
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") | |||
projects_by_root = projects.group_by(&:root) | |||
projects_by_root.keys.sort.each do |root| | |||
options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root))) | |||
projects_by_root[root].sort.each do |project| | |||
next if project == root | |||
options << content_tag('option', '» ' + h(project.name), :value => project.id) | |||
end | |||
options << project_tree_options_for_select(projects) do |p| | |||
{:disabled => (user.projects.include?(p))} | |||
end | |||
options | |||
end |
@@ -16,22 +16,6 @@ | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
module WikiHelper | |||
def render_page_hierarchy(pages, node=nil) | |||
content = '' | |||
if pages[node] | |||
content << "<ul class=\"pages-hierarchy\">\n" | |||
pages[node].each do |page| | |||
content << "<li>" | |||
content << link_to(h(page.pretty_title), {:action => 'index', :page => page.title}, | |||
:title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) | |||
content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id] | |||
content << "</li>\n" | |||
end | |||
content << "</ul>\n" | |||
end | |||
content | |||
end | |||
def html_diff(wdiff) | |||
words = wdiff.words.collect{|word| h(word)} |
@@ -1,5 +1,5 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006-2007 Jean-Philippe Lang | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2008 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
@@ -15,19 +15,5 @@ | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
class AWSProjectWithRepository < ActionWebService::Struct | |||
member :id, :int | |||
member :identifier, :string | |||
member :name, :string | |||
member :is_public, :bool | |||
member :repository, Repository | |||
end | |||
class SysApi < ActionWebService::API::Base | |||
api_method :projects_with_repository_enabled, | |||
:expects => [], | |||
:returns => [[AWSProjectWithRepository]] | |||
api_method :repository_created, | |||
:expects => [:string, :string, :string], | |||
:returns => [:int] | |||
module WorkflowsHelper | |||
end |
@@ -30,12 +30,14 @@ class Attachment < ActiveRecord::Base | |||
acts_as_activity_provider :type => 'files', | |||
:permission => :view_files, | |||
:author_key => :author_id, | |||
:find_options => {:select => "#{Attachment.table_name}.*", | |||
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " + | |||
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"} | |||
acts_as_activity_provider :type => 'documents', | |||
:permission => :view_documents, | |||
:author_key => :author_id, | |||
:find_options => {:select => "#{Attachment.table_name}.*", | |||
:joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " + | |||
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"} | |||
@@ -70,7 +72,7 @@ class Attachment < ActiveRecord::Base | |||
File.open(diskfile, "wb") do |f| | |||
f.write(@temp_file.read) | |||
end | |||
self.digest = Digest::MD5.hexdigest(File.read(diskfile)) | |||
self.digest = self.class.digest(diskfile) | |||
end | |||
# Don't save the content type if it's longer than the authorized length | |||
if self.content_type && self.content_type.length > 255 | |||
@@ -96,6 +98,14 @@ class Attachment < ActiveRecord::Base | |||
container.project | |||
end | |||
def visible?(user=User.current) | |||
container.attachments_visible?(user) | |||
end | |||
def deletable?(user=User.current) | |||
container.attachments_deletable?(user) | |||
end | |||
def image? | |||
self.filename =~ /\.(jpe?g|gif|png)$/i | |||
end | |||
@@ -131,4 +141,11 @@ private | |||
end | |||
df | |||
end | |||
# Returns the MD5 digest of the file at given path | |||
def self.digest(filename) | |||
File.open(filename, 'rb') do |f| | |||
Digest::MD5.hexdigest(f.read) | |||
end | |||
end | |||
end |
@@ -38,7 +38,8 @@ class AuthSource < ActiveRecord::Base | |||
begin | |||
logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug? | |||
attrs = source.authenticate(login, password) | |||
rescue | |||
rescue => e | |||
logger.error "Error during authentication: #{e.message}" | |||
attrs = nil | |||
end | |||
return attrs if attrs |
@@ -91,6 +91,8 @@ class AuthSourceLdap < AuthSource | |||
end | |||
def self.get_attr(entry, attr_name) | |||
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] | |||
if !attr_name.blank? | |||
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] | |||
end | |||
end | |||
end |
@@ -1,5 +1,5 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006-2007 Jean-Philippe Lang | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2008 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
@@ -19,13 +19,13 @@ require 'iconv' | |||
class Changeset < ActiveRecord::Base | |||
belongs_to :repository | |||
belongs_to :user | |||
has_many :changes, :dependent => :delete_all | |||
has_and_belongs_to_many :issues | |||
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))}, | |||
:description => :comments, | |||
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, | |||
:description => :long_comments, | |||
:datetime => :committed_on, | |||
:author => :committer, | |||
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}} | |||
acts_as_searchable :columns => 'comments', | |||
@@ -34,6 +34,7 @@ class Changeset < ActiveRecord::Base | |||
:date_column => 'committed_on' | |||
acts_as_activity_provider :timestamp => "#{table_name}.committed_on", | |||
:author_key => :user_id, | |||
:find_options => {:include => {:repository => :project}} | |||
validates_presence_of :repository_id, :revision, :committed_on, :commit_date | |||
@@ -57,6 +58,14 @@ class Changeset < ActiveRecord::Base | |||
repository.project | |||
end | |||
def author | |||
user || committer.to_s.split('<').first | |||
end | |||
def before_create | |||
self.user = repository.find_committer_user(committer) | |||
end | |||
def after_create | |||
scan_comment_for_issue_ids | |||
end | |||
@@ -96,12 +105,11 @@ class Changeset < ActiveRecord::Base | |||
issue.reload | |||
# don't change the status is the issue is closed | |||
next if issue.status.is_closed? | |||
user = committer_user || User.anonymous | |||
csettext = "r#{self.revision}" | |||
if self.scmid && (! (csettext =~ /^r[0-9]+$/)) | |||
csettext = "commit:\"#{self.scmid}\"" | |||
end | |||
journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext)) | |||
journal = issue.init_journal(user || User.anonymous, l(:text_status_changed_by_changeset, csettext)) | |||
issue.status = fix_status | |||
issue.done_ratio = done_ratio if done_ratio | |||
issue.save | |||
@@ -113,15 +121,13 @@ class Changeset < ActiveRecord::Base | |||
self.issues = referenced_issues.uniq | |||
end | |||
# Returns the Redmine User corresponding to the committer | |||
def committer_user | |||
if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/ | |||
username, email = $1.strip, $3 | |||
u = User.find_by_login(username) | |||
u ||= User.find_by_mail(email) unless email.blank? | |||
u | |||
end | |||
def short_comments | |||
@short_comments || split_comments.first | |||
end | |||
def long_comments | |||
@long_comments || split_comments.last | |||
end | |||
# Returns the previous changeset | |||
@@ -140,7 +146,14 @@ class Changeset < ActiveRecord::Base | |||
end | |||
private | |||
def split_comments | |||
comments =~ /\A(.+?)\r?\n(.*)$/m | |||
@short_comments = $1 || comments | |||
@long_comments = $2.to_s.strip | |||
return @short_comments, @long_comments | |||
end | |||
def self.to_utf8(str) | |||
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii | |||
encoding = Setting.commit_logs_encoding.to_s.strip |
@@ -41,8 +41,6 @@ class CustomField < ActiveRecord::Base | |||
end | |||
def before_validation | |||
# remove empty values | |||
self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact | |||
# make sure these fields are not searchable | |||
self.searchable = false if %w(int float date bool).include?(field_format) | |||
true | |||
@@ -59,11 +57,49 @@ class CustomField < ActiveRecord::Base | |||
v.custom_field.is_required = false | |||
errors.add(:default_value, :activerecord_error_invalid) unless v.valid? | |||
end | |||
# Makes possible_values accept a multiline string | |||
def possible_values=(arg) | |||
if arg.is_a?(Array) | |||
write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?}) | |||
else | |||
self.possible_values = arg.to_s.split(/[\n\r]+/) | |||
end | |||
end | |||
# Returns a ORDER BY clause that can used to sort customized | |||
# objects by their value of the custom field. | |||
# Returns false, if the custom field can not be used for sorting. | |||
def order_statement | |||
case field_format | |||
when 'string', 'text', 'list', 'date', 'bool' | |||
# COALESCE is here to make sure that blank and NULL values are sorted equally | |||
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" + | |||
" WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" + | |||
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + | |||
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')" | |||
when 'int', 'float' | |||
# Make the database cast values into numeric | |||
# Postgresql will raise an error if a value can not be casted! | |||
# CustomValue validations should ensure that it doesn't occur | |||
"(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" + | |||
" WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" + | |||
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + | |||
" AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)" | |||
else | |||
nil | |||
end | |||
end | |||
def <=>(field) | |||
position <=> field.position | |||
end | |||
def self.customized_class | |||
self.name =~ /^(.+)CustomField$/ | |||
begin; $1.constantize; rescue nil; end | |||
end | |||
# to move in project_custom_field | |||
def self.for_all | |||
find(:all, :conditions => ["is_for_all=?", true], :order => 'position') |
@@ -30,6 +30,18 @@ class CustomValue < ActiveRecord::Base | |||
self.value == '1' | |||
end | |||
def editable? | |||
custom_field.editable? | |||
end | |||
def required? | |||
custom_field.is_required? | |||
end | |||
def to_s | |||
value.to_s | |||
end | |||
protected | |||
def validate | |||
if value.blank? |
@@ -18,7 +18,7 @@ | |||
class Document < ActiveRecord::Base | |||
belongs_to :project | |||
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id" | |||
has_many :attachments, :as => :container, :dependent => :destroy | |||
acts_as_attachable :delete_permission => :manage_documents | |||
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project | |||
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, | |||
@@ -28,4 +28,10 @@ class Document < ActiveRecord::Base | |||
validates_presence_of :project, :title, :category | |||
validates_length_of :title, :maximum => 60 | |||
def after_initialize | |||
if new_record? | |||
self.category ||= Enumeration.default('DCAT') | |||
end | |||
end | |||
end |
@@ -44,7 +44,9 @@ class Enumeration < ActiveRecord::Base | |||
end | |||
def before_save | |||
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default? | |||
if is_default? && is_default_changed? | |||
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) | |||
end | |||
end | |||
def objects_count |
@@ -26,13 +26,13 @@ class Issue < ActiveRecord::Base | |||
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' | |||
has_many :journals, :as => :journalized, :dependent => :destroy | |||
has_many :attachments, :as => :container, :dependent => :destroy | |||
has_many :time_entries, :dependent => :delete_all | |||
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" | |||
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all | |||
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all | |||
acts_as_attachable :after_remove => :attachment_removed | |||
acts_as_customizable | |||
acts_as_watchable | |||
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], | |||
@@ -40,15 +40,25 @@ class Issue < ActiveRecord::Base | |||
# sort by id so that limited eager loading doesn't break with postgresql | |||
:order_column => "#{table_name}.id" | |||
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, | |||
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} | |||
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, | |||
:type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } | |||
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]} | |||
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, | |||
:author_key => :author_id | |||
validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status | |||
validates_presence_of :subject, :priority, :project, :tracker, :author, :status | |||
validates_length_of :subject, :maximum => 255 | |||
validates_inclusion_of :done_ratio, :in => 0..100 | |||
validates_numericality_of :estimated_hours, :allow_nil => true | |||
named_scope :visible, lambda {|*args| { :include => :project, | |||
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } } | |||
# Returns true if usr or current user is allowed to view the issue | |||
def visible?(usr=nil) | |||
(usr || User.current).allowed_to?(:view_issues, self.project) | |||
end | |||
def after_initialize | |||
if new_record? | |||
# set default values for new records only | |||
@@ -69,34 +79,43 @@ class Issue < ActiveRecord::Base | |||
self | |||
end | |||
# Move an issue to a new project and tracker | |||
def move_to(new_project, new_tracker = nil) | |||
# Moves/copies an issue to a new project and tracker | |||
# Returns the moved/copied issue on success, false on failure | |||
def move_to(new_project, new_tracker = nil, options = {}) | |||
options ||= {} | |||
issue = options[:copy] ? self.clone : self | |||
transaction do | |||
if new_project && project_id != new_project.id | |||
if new_project && issue.project_id != new_project.id | |||
# delete issue relations | |||
unless Setting.cross_project_issue_relations? | |||
self.relations_from.clear | |||
self.relations_to.clear | |||
issue.relations_from.clear | |||
issue.relations_to.clear | |||
end | |||
# issue is moved to another project | |||
# reassign to the category with same name if any | |||
new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name) | |||
self.category = new_category | |||
self.fixed_version = nil | |||
self.project = new_project | |||
new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name) | |||
issue.category = new_category | |||
issue.fixed_version = nil | |||
issue.project = new_project | |||
end | |||
if new_tracker | |||
self.tracker = new_tracker | |||
issue.tracker = new_tracker | |||
end | |||
if options[:copy] | |||
issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} | |||
issue.status = self.status | |||
end | |||
if save | |||
# Manually update project_id on related time entries | |||
TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) | |||
if issue.save | |||
unless options[:copy] | |||
# Manually update project_id on related time entries | |||
TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) | |||
end | |||
else | |||
rollback_db_transaction | |||
Issue.connection.rollback_db_transaction | |||
return false | |||
end | |||
end | |||
return true | |||
return issue | |||
end | |||
def priority_id=(pid) | |||
@@ -194,6 +213,11 @@ class Issue < ActiveRecord::Base | |||
self.status.is_closed? | |||
end | |||
# Returns true if the issue is overdue | |||
def overdue? | |||
!due_date.nil? && (due_date < Date.today) && !status.is_closed? | |||
end | |||
# Users the issue can be assigned to | |||
def assignable_users | |||
project.assignable_users | |||
@@ -251,13 +275,18 @@ class Issue < ActiveRecord::Base | |||
@soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min | |||
end | |||
def self.visible_by(usr) | |||
with_scope(:find => { :conditions => Project.visible_by(usr) }) do | |||
yield | |||
end | |||
end | |||
def to_s | |||
"#{tracker} ##{id}: #{subject}" | |||
end | |||
private | |||
# Callback on attachment deletion | |||
def attachment_removed(obj) | |||
journal = init_journal(User.current) | |||
journal.details << JournalDetail.new(:property => 'attachment', | |||
:prop_key => obj.id, | |||
:old_value => obj.filename) | |||
journal.save | |||
end | |||
end |
@@ -35,6 +35,8 @@ class IssueRelation < ActiveRecord::Base | |||
validates_numericality_of :delay, :allow_nil => true | |||
validates_uniqueness_of :issue_to_id, :scope => :issue_from_id | |||
attr_protected :issue_from_id, :issue_to_id | |||
def validate | |||
if issue_from && issue_to | |||
errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id |
@@ -25,8 +25,8 @@ class IssueStatus < ActiveRecord::Base | |||
validates_length_of :name, :maximum => 30 | |||
validates_format_of :name, :with => /^[\w\s\'\-]*$/i | |||
def before_save | |||
IssueStatus.update_all "is_default=#{connection.quoted_false}" if self.is_default? | |||
def after_save | |||
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default? | |||
end | |||
# Returns the default status for new issues |
@@ -33,6 +33,7 @@ class Journal < ActiveRecord::Base | |||
acts_as_activity_provider :type => 'issues', | |||
:permission => :view_issues, | |||
:author_key => :user_id, | |||
:find_options => {:include => [{:issue => :project}, :details, :user], | |||
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + | |||
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} |
@@ -16,6 +16,7 @@ | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
class MailHandler < ActionMailer::Base | |||
include ActionView::Helpers::SanitizeHelper | |||
class UnauthorizedAction < StandardError; end | |||
class MissingInformation < StandardError; end | |||
@@ -31,7 +32,7 @@ class MailHandler < ActionMailer::Base | |||
@@handler_options[:allow_override] ||= [] | |||
# Project needs to be overridable if not specified | |||
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) | |||
# Status needs to be overridable if not specified | |||
# Status overridable by default | |||
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) | |||
super email | |||
end | |||
@@ -39,7 +40,7 @@ class MailHandler < ActionMailer::Base | |||
# Processes incoming emails | |||
def receive(email) | |||
@email = email | |||
@user = User.find_active(:first, :conditions => {:mail => email.from.first}) | |||
@user = User.active.find_by_mail(email.from.first.to_s.strip) | |||
unless @user | |||
# Unknown user => the email is ignored | |||
# TODO: ability to create the user's account | |||
@@ -52,11 +53,24 @@ class MailHandler < ActionMailer::Base | |||
private | |||
MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@} | |||
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]} | |||
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]} | |||
def dispatch | |||
if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE) | |||
receive_issue_update(m[1].to_i) | |||
headers = [email.in_reply_to, email.references].flatten.compact | |||
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE} | |||
klass, object_id = $1, $2.to_i | |||
method_name = "receive_#{klass}_reply" | |||
if self.class.private_instance_methods.include?(method_name) | |||
send method_name, object_id | |||
else | |||
# ignoring it | |||
end | |||
elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE) | |||
receive_issue_reply(m[1].to_i) | |||
elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE) | |||
receive_message_reply(m[1].to_i) | |||
else | |||
receive_issue | |||
end | |||
@@ -78,16 +92,30 @@ class MailHandler < ActionMailer::Base | |||
tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first) | |||
category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category))) | |||
priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority))) | |||
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) || IssueStatus.default | |||
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) | |||
# check permission | |||
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) | |||
issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority, :status => status) | |||
issue.subject = email.subject.chomp | |||
issue.description = email.plain_text_body.chomp | |||
issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority) | |||
# check workflow | |||
if status && issue.new_statuses_allowed_to(user).include?(status) | |||
issue.status = status | |||
end | |||
issue.subject = email.subject.chomp.toutf8 | |||
issue.description = plain_text_body | |||
# custom fields | |||
issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c| | |||
if value = get_keyword(c.name, :override => true) | |||
h[c.id] = value | |||
end | |||
h | |||
end | |||
issue.save! | |||
add_attachments(issue) | |||
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info | |||
# add To and Cc as watchers | |||
add_watchers(issue) | |||
# send notification after adding watchers so that they can reply to Redmine | |||
Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added') | |||
issue | |||
end | |||
@@ -102,7 +130,7 @@ class MailHandler < ActionMailer::Base | |||
end | |||
# Adds a note to an existing issue | |||
def receive_issue_update(issue_id) | |||
def receive_issue_reply(issue_id) | |||
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) | |||
issue = Issue.find_by_id(issue_id) | |||
@@ -112,15 +140,45 @@ class MailHandler < ActionMailer::Base | |||
raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project) | |||
# add the note | |||
journal = issue.init_journal(user, email.plain_text_body.chomp) | |||
journal = issue.init_journal(user, plain_text_body) | |||
add_attachments(issue) | |||
issue.status = status unless status.nil? | |||
# check workflow | |||
if status && issue.new_statuses_allowed_to(user).include?(status) | |||
issue.status = status | |||
end | |||
issue.save! | |||
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info | |||
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') | |||
journal | |||
end | |||
# Reply will be added to the issue | |||
def receive_journal_reply(journal_id) | |||
journal = Journal.find_by_id(journal_id) | |||
if journal && journal.journalized_type == 'Issue' | |||
receive_issue_reply(journal.journalized_id) | |||
end | |||
end | |||
# Receives a reply to a forum message | |||
def receive_message_reply(message_id) | |||
message = Message.find_by_id(message_id) | |||
if message | |||
message = message.root | |||
if user.allowed_to?(:add_messages, message.project) && !message.locked? | |||
reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip, | |||
:content => plain_text_body) | |||
reply.author = user | |||
reply.board = message.board | |||
message.children << reply | |||
add_attachments(reply) | |||
reply | |||
else | |||
raise UnauthorizedAction | |||
end | |||
end | |||
end | |||
def add_attachments(obj) | |||
if email.has_attachments? | |||
email.attachments.each do |attachment| | |||
@@ -132,22 +190,50 @@ class MailHandler < ActionMailer::Base | |||
end | |||
end | |||
def get_keyword(attr) | |||
if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i | |||
$1.strip | |||
elsif !@@handler_options[:issue][attr].blank? | |||
@@handler_options[:issue][attr] | |||
# Adds To and Cc as watchers of the given object if the sender has the | |||
# appropriate permission | |||
def add_watchers(obj) | |||
if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) | |||
addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} | |||
unless addresses.empty? | |||
watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses]) | |||
watchers.each {|w| obj.add_watcher(w)} | |||
end | |||
end | |||
end | |||
end | |||
class TMail::Mail | |||
# Returns body of the first plain text part found if any | |||
def get_keyword(attr, options={}) | |||
@keywords ||= {} | |||
if @keywords.has_key?(attr) | |||
@keywords[attr] | |||
else | |||
@keywords[attr] = begin | |||
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}:[ \t]*(.+)\s*$/i, '') | |||
$1.strip | |||
elsif !@@handler_options[:issue][attr].blank? | |||
@@handler_options[:issue][attr] | |||
end | |||
end | |||
end | |||
end | |||
# Returns the text/plain part of the email | |||
# If not found (eg. HTML-only email), returns the body with tags removed | |||
def plain_text_body | |||
return @plain_text_body unless @plain_text_body.nil? | |||
p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten | |||
plain = p.detect {|c| c.content_type == 'text/plain'} | |||
@plain_text_body = plain.nil? ? self.body : plain.body | |||
parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten | |||
if parts.empty? | |||
parts << @email | |||
end | |||
plain_text_part = parts.detect {|p| p.content_type == 'text/plain'} | |||
if plain_text_part.nil? | |||
# no text/plain part found, assuming html-only email | |||
# strip html tags and remove doctype directive | |||
@plain_text_body = strip_tags(@email.body.to_s) | |||
@plain_text_body.gsub! %r{^<!DOCTYPE .*$}, '' | |||
else | |||
@plain_text_body = plain_text_part.body.to_s | |||
end | |||
@plain_text_body.strip! | |||
end | |||
end | |||
@@ -5,12 +5,12 @@ | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 2 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
@@ -19,15 +19,17 @@ class Mailer < ActionMailer::Base | |||
helper :application | |||
helper :issues | |||
helper :custom_fields | |||
include ActionController::UrlWriter | |||
def issue_add(issue) | |||
def issue_add(issue) | |||
redmine_headers 'Project' => issue.project.identifier, | |||
'Issue-Id' => issue.id, | |||
'Issue-Author' => issue.author.login | |||
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to | |||
recipients issue.recipients | |||
message_id issue | |||
recipients issue.recipients | |||
cc(issue.watcher_recipients - @recipients) | |||
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}" | |||
body :issue => issue, | |||
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) | |||
@@ -39,6 +41,9 @@ class Mailer < ActionMailer::Base | |||
'Issue-Id' => issue.id, | |||
'Issue-Author' => issue.author.login | |||
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to | |||
message_id journal | |||
references issue | |||
@author = journal.user | |||
recipients issue.recipients | |||
# Watchers in cc | |||
cc(issue.watcher_recipients - @recipients) | |||
@@ -50,16 +55,16 @@ class Mailer < ActionMailer::Base | |||
:journal => journal, | |||
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) | |||
end | |||
def reminder(user, issues, days) | |||
set_language_if_valid user.language | |||
recipients user.mail | |||
subject l(:mail_subject_reminder, issues.size) | |||
body :issues => issues, | |||
:days => days, | |||
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'issues.due_date', :sort_order => 'asc') | |||
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc') | |||
end | |||
def document_added(document) | |||
redmine_headers 'Project' => document.project.identifier | |||
recipients document.project.recipients | |||
@@ -67,12 +72,15 @@ class Mailer < ActionMailer::Base | |||
body :document => document, | |||
:document_url => url_for(:controller => 'documents', :action => 'show', :id => document) | |||
end | |||
def attachments_added(attachments) | |||
container = attachments.first.container | |||
added_to = '' | |||
added_to_url = '' | |||
case container.class.name | |||
when 'Project' | |||
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container) | |||
added_to = "#{l(:label_project)}: #{container}" | |||
when 'Version' | |||
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id) | |||
added_to = "#{l(:label_version)}: #{container.name}" | |||
@@ -90,6 +98,7 @@ class Mailer < ActionMailer::Base | |||
def news_added(news) | |||
redmine_headers 'Project' => news.project.identifier | |||
message_id news | |||
recipients news.project.recipients | |||
subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}" | |||
body :news => news, | |||
@@ -99,12 +108,14 @@ class Mailer < ActionMailer::Base | |||
def message_posted(message, recipients) | |||
redmine_headers 'Project' => message.project.identifier, | |||
'Topic-Id' => (message.parent_id || message.id) | |||
message_id message | |||
references message.parent unless message.parent.nil? | |||
recipients(recipients) | |||
subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}" | |||
subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}" | |||
body :message => message, | |||
:message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root) | |||
end | |||
def account_information(user, password) | |||
set_language_if_valid user.language | |||
recipients user.mail | |||
@@ -113,10 +124,10 @@ class Mailer < ActionMailer::Base | |||
:password => password, | |||
:login_url => url_for(:controller => 'account', :action => 'login') | |||
end | |||
def account_activation_request(user) | |||
# Send the email to all active administrators | |||
recipients User.find_active(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact | |||
recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact | |||
subject l(:mail_subject_account_activation_request, Setting.app_title) | |||
body :user => user, | |||
:url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc') | |||
@@ -128,7 +139,7 @@ class Mailer < ActionMailer::Base | |||
subject l(:mail_subject_lost_password, Setting.app_title) | |||
body :token => token, | |||
:url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value) | |||
end | |||
end | |||
def register(token) | |||
set_language_if_valid(token.user.language) | |||
@@ -137,7 +148,7 @@ class Mailer < ActionMailer::Base | |||
body :token => token, | |||
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value) | |||
end | |||
def test(user) | |||
set_language_if_valid(user.language) | |||
recipients user.mail | |||
@@ -148,12 +159,20 @@ class Mailer < ActionMailer::Base | |||
# Overrides default deliver! method to prevent from sending an email | |||
# with no recipient, cc or bcc | |||
def deliver!(mail = @mail) | |||
return false if (recipients.nil? || recipients.empty?) && | |||
return false if (recipients.nil? || recipients.empty?) && | |||
(cc.nil? || cc.empty?) && | |||
(bcc.nil? || bcc.empty?) | |||
super | |||
# Set Message-Id and References | |||
if @message_id_object | |||
mail.message_id = self.class.message_id_for(@message_id_object) | |||
end | |||
if @references_objects | |||
mail.references = @references_objects.collect {|o| self.class.message_id_for(o)} | |||
end | |||
super(mail) | |||
end | |||
# Sends reminders to issue assignees | |||
# Available options: | |||
# * :days => how many days in the future to remind about (defaults to 7) | |||
@@ -163,13 +182,13 @@ class Mailer < ActionMailer::Base | |||
days = options[:days] || 7 | |||
project = options[:project] ? Project.find(options[:project]) : nil | |||
tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil | |||
s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date] | |||
s << "#{Issue.table_name}.assigned_to_id IS NOT NULL" | |||
s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}" | |||
s << "#{Issue.table_name}.project_id = #{project.id}" if project | |||
s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker | |||
issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker], | |||
:conditions => s.conditions | |||
).group_by(&:assigned_to) | |||
@@ -183,45 +202,96 @@ class Mailer < ActionMailer::Base | |||
super | |||
set_language_if_valid Setting.default_language | |||
from Setting.mail_from | |||
default_url_options[:host] = Setting.host_name | |||
# URL options | |||
h = Setting.host_name | |||
h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank? | |||
default_url_options[:host] = h | |||
default_url_options[:protocol] = Setting.protocol | |||
# Common headers | |||
headers 'X-Mailer' => 'Redmine', | |||
'X-Redmine-Host' => Setting.host_name, | |||
'X-Redmine-Site' => Setting.app_title | |||
end | |||
# Appends a Redmine header field (name is prepended with 'X-Redmine-') | |||
def redmine_headers(h) | |||
h.each { |k,v| headers["X-Redmine-#{k}"] = v } | |||
end | |||
# Overrides the create_mail method | |||
def create_mail | |||
# Removes the current user from the recipients and cc | |||
# if he doesn't want to receive notifications about what he does | |||
if User.current.pref[:no_self_notified] | |||
recipients.delete(User.current.mail) if recipients | |||
cc.delete(User.current.mail) if cc | |||
@author ||= User.current | |||
if @author.pref[:no_self_notified] | |||
recipients.delete(@author.mail) if recipients | |||
cc.delete(@author.mail) if cc | |||
end | |||
# Blind carbon copy recipients | |||
if Setting.bcc_recipients? | |||
bcc([recipients, cc].flatten.compact.uniq) | |||
recipients [] | |||
cc [] | |||
end | |||
end | |||
super | |||
end | |||
# Renders a message with the corresponding layout | |||
def render_message(method_name, body) | |||
layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml' | |||
layout = method_name.to_s.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml' | |||
body[:content_for_layout] = render(:file => method_name, :body => body) | |||
ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true) | |||
end | |||
# for the case of plain text only | |||
def body(*params) | |||
value = super(*params) | |||
if Setting.plain_text_mail? | |||
templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}") | |||
unless String === @body or templates.empty? | |||
template = File.basename(templates.first) | |||
@body[:content_for_layout] = render(:file => template, :body => @body) | |||
@body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true) | |||
return @body | |||
end | |||
end | |||
return value | |||
end | |||
# Makes partial rendering work with Rails 1.2 (retro-compatibility) | |||
def self.controller_path | |||
'' | |||
end unless respond_to?('controller_path') | |||
# Returns a predictable Message-Id for the given object | |||
def self.message_id_for(object) | |||
# id + timestamp should reduce the odds of a collision | |||
# as far as we don't send multiple emails for the same object | |||
hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{object.created_on.strftime("%Y%m%d%H%M%S")}" | |||
host = Setting.mail_from.to_s.gsub(%r{^.*@}, '') | |||
host = "#{::Socket.gethostname}.redmine" if host.empty? | |||
"<#{hash}@#{host}>" | |||
end | |||
private | |||
def message_id(object) | |||
@message_id_object = object | |||
end | |||
def references(object) | |||
@references_objects ||= [] | |||
@references_objects << object | |||
end | |||
end | |||
# Patch TMail so that message_id is not overwritten | |||
module TMail | |||
class Mail | |||
def add_message_id( fqdn = nil ) | |||
self.message_id ||= ::TMail::new_message_id(fqdn) | |||
end | |||
end | |||
end |
@@ -19,11 +19,11 @@ class Message < ActiveRecord::Base | |||
belongs_to :board | |||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' | |||
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC" | |||
has_many :attachments, :as => :container, :dependent => :destroy | |||
acts_as_attachable | |||
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' | |||
acts_as_searchable :columns => ['subject', 'content'], | |||
:include => {:board, :project}, | |||
:include => {:board => :project}, | |||
:project_key => 'project_id', | |||
:date_column => "#{table_name}.created_on" | |||
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, | |||
@@ -32,7 +32,8 @@ class Message < ActiveRecord::Base | |||
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : | |||
{:id => o.parent_id, :anchor => "message-#{o.id}"})} | |||
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]} | |||
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}, | |||
:author_key => :author_id | |||
acts_as_watchable | |||
attr_protected :locked, :sticky | |||
@@ -71,6 +72,14 @@ class Message < ActiveRecord::Base | |||
def project | |||
board.project | |||
end | |||
def editable_by?(usr) | |||
usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project))) | |||
end | |||
def destroyable_by?(usr) | |||
usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project))) | |||
end | |||
private | |||
@@ -1,5 +1,5 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006 Jean-Philippe Lang | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2008 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
@@ -26,10 +26,11 @@ class News < ActiveRecord::Base | |||
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project | |||
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} | |||
acts_as_activity_provider :find_options => {:include => [:project, :author]} | |||
acts_as_activity_provider :find_options => {:include => [:project, :author]}, | |||
:author_key => :author_id | |||
# returns latest news for projects visible by user | |||
def self.latest(user=nil, count=5) | |||
find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") | |||
def self.latest(user = User.current, count = 5) | |||
find(:all, :limit => count, :conditions => Project.allowed_to_condition(user, :view_news), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") | |||
end | |||
end |
@@ -43,7 +43,9 @@ class Project < ActiveRecord::Base | |||
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", | |||
:association_foreign_key => 'custom_field_id' | |||
acts_as_tree :order => "name", :counter_cache => true | |||
acts_as_nested_set :order => 'name', :dependent => :destroy | |||
acts_as_attachable :view_permission => :view_files, | |||
:delete_permission => :manage_files | |||
acts_as_customizable | |||
acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil | |||
@@ -58,12 +60,15 @@ class Project < ActiveRecord::Base | |||
validates_associated :repository, :wiki | |||
validates_length_of :name, :maximum => 30 | |||
validates_length_of :homepage, :maximum => 255 | |||
validates_length_of :identifier, :in => 3..20 | |||
validates_length_of :identifier, :in => 2..20 | |||
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ | |||
before_destroy :delete_all_members | |||
named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } | |||
named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} | |||
named_scope :public, { :conditions => { :is_public => true } } | |||
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } } | |||
def identifier=(identifier) | |||
super unless identifier_frozen? | |||
@@ -76,7 +81,7 @@ class Project < ActiveRecord::Base | |||
def issues_with_subprojects(include_subprojects=false) | |||
conditions = nil | |||
if include_subprojects | |||
ids = [id] + child_ids | |||
ids = [id] + descendants.collect(&:id) | |||
conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"] | |||
end | |||
conditions ||= ["#{Project.table_name}.id = ?", id] | |||
@@ -108,9 +113,15 @@ class Project < ActiveRecord::Base | |||
def self.allowed_to_condition(user, permission, options={}) | |||
statements = [] | |||
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" | |||
if perm = Redmine::AccessControl.permission(permission) | |||
unless perm.project_module.nil? | |||
# If the permission belongs to a project module, make sure the module is enabled | |||
base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)" | |||
end | |||
end | |||
if options[:project] | |||
project_statement = "#{Project.table_name}.id = #{options[:project].id}" | |||
project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects] | |||
project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] | |||
base_statement = "(#{project_statement}) AND (#{base_statement})" | |||
end | |||
if user.admin? | |||
@@ -133,7 +144,7 @@ class Project < ActiveRecord::Base | |||
def project_condition(with_subprojects) | |||
cond = "#{Project.table_name}.id = #{id}" | |||
cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects | |||
cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects | |||
cond | |||
end | |||
@@ -156,6 +167,7 @@ class Project < ActiveRecord::Base | |||
self.status == STATUS_ACTIVE | |||
end | |||
# Archives the project and its descendants recursively | |||
def archive | |||
# Archive subprojects if any | |||
children.each do |subproject| | |||
@@ -164,21 +176,62 @@ class Project < ActiveRecord::Base | |||
update_attribute :status, STATUS_ARCHIVED | |||
end | |||
# Unarchives the project | |||
# All its ancestors must be active | |||
def unarchive | |||
return false if parent && !parent.active? | |||
return false if ancestors.detect {|a| !a.active?} | |||
update_attribute :status, STATUS_ACTIVE | |||
end | |||
def active_children | |||
children.select {|child| child.active?} | |||
# Returns an array of projects the project can be moved to | |||
def possible_parents | |||
@possible_parents ||= (Project.active.find(:all) - self_and_descendants) | |||
end | |||
# Sets the parent of the project | |||
# Argument can be either a Project, a String, a Fixnum or nil | |||
def set_parent!(p) | |||
unless p.nil? || p.is_a?(Project) | |||
if p.to_s.blank? | |||
p = nil | |||
else | |||
p = Project.find_by_id(p) | |||
return false unless p | |||
end | |||
end | |||
if p == parent && !p.nil? | |||
# Nothing to do | |||
true | |||
elsif p.nil? || (p.active? && move_possible?(p)) | |||
# Insert the project so that target's children or root projects stay alphabetically sorted | |||
sibs = (p.nil? ? self.class.roots : p.children) | |||
to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase } | |||
if to_be_inserted_before | |||
move_to_left_of(to_be_inserted_before) | |||
elsif p.nil? | |||
if sibs.empty? | |||
# move_to_root adds the project in first (ie. left) position | |||
move_to_root | |||
else | |||
move_to_right_of(sibs.last) unless self == sibs.last | |||
end | |||
else | |||
# move_to_child_of adds the project in last (ie.right) position | |||
move_to_child_of(p) | |||
end | |||
true | |||
else | |||
# Can not move to the given target | |||
false | |||
end | |||
end | |||
# Returns an array of the trackers used by the project and its sub projects | |||
# Returns an array of the trackers used by the project and its active sub projects | |||
def rolled_up_trackers | |||
@rolled_up_trackers ||= | |||
Tracker.find(:all, :include => :projects, | |||
:select => "DISTINCT #{Tracker.table_name}.*", | |||
:conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id], | |||
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt], | |||
:order => "#{Tracker.table_name}.position") | |||
end | |||
@@ -217,7 +270,7 @@ class Project < ActiveRecord::Base | |||
# Returns a short description of the projects (first lines) | |||
def short_description(length = 255) | |||
description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description | |||
description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description | |||
end | |||
def allows_to?(action) | |||
@@ -249,8 +302,6 @@ class Project < ActiveRecord::Base | |||
protected | |||
def validate | |||
errors.add(parent_id, " must be a root project") if parent and parent.parent | |||
errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0 | |||
errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/) | |||
end | |||
@@ -35,7 +35,7 @@ class QueryCustomFieldColumn < QueryColumn | |||
def initialize(custom_field) | |||
self.name = "cf_#{custom_field.id}".to_sym | |||
self.sortable = false | |||
self.sortable = custom_field.order_statement || false | |||
@cf = custom_field | |||
end | |||
@@ -98,10 +98,10 @@ class Query < ActiveRecord::Base | |||
QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'), | |||
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), | |||
QueryColumn.new(:author), | |||
QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"), | |||
QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname"]), | |||
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), | |||
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"), | |||
QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'), | |||
QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc'), | |||
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), | |||
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), | |||
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), | |||
@@ -165,6 +165,10 @@ class Query < ActiveRecord::Base | |||
end | |||
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty? | |||
@available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty? | |||
if User.current.logged? | |||
@available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] } | |||
end | |||
if project | |||
# project specific filters | |||
@@ -174,8 +178,8 @@ class Query < ActiveRecord::Base | |||
unless @project.versions.empty? | |||
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } | |||
end | |||
unless @project.active_children.empty? | |||
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } } | |||
unless @project.descendants.active.empty? | |||
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } } | |||
end | |||
add_custom_fields_filters(@project.all_issue_custom_fields) | |||
else | |||
@@ -257,7 +261,7 @@ class Query < ActiveRecord::Base | |||
def project_statement | |||
project_clauses = [] | |||
if project && !@project.active_children.empty? | |||
if project && !@project.descendants.active.empty? | |||
ids = [project.id] | |||
if has_filter?("subproject_id") | |||
case operator_for("subproject_id") | |||
@@ -268,16 +272,16 @@ class Query < ActiveRecord::Base | |||
# main project only | |||
else | |||
# all subprojects | |||
ids += project.child_ids | |||
ids += project.descendants.collect(&:id) | |||
end | |||
elsif Setting.display_subprojects_issues? | |||
ids += project.child_ids | |||
ids += project.descendants.collect(&:id) | |||
end | |||
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') | |||
elsif project | |||
project_clauses << "#{Project.table_name}.id = %d" % project.id | |||
end | |||
project_clauses << Project.visible_by(User.current) | |||
project_clauses << Project.allowed_to_condition(User.current, :view_issues) | |||
project_clauses.join(' AND ') | |||
end | |||
@@ -288,74 +292,34 @@ class Query < ActiveRecord::Base | |||
next if field == "subproject_id" | |||
v = values_for(field).clone | |||
next unless v and !v.empty? | |||
operator = operator_for(field) | |||
# "me" value subsitution | |||
if %w(assigned_to_id author_id watcher_id).include?(field) | |||
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") | |||
end | |||
sql = '' | |||
is_custom_filter = false | |||
if field =~ /^cf_(\d+)$/ | |||
# custom field | |||
db_table = CustomValue.table_name | |||
db_field = 'value' | |||
is_custom_filter = true | |||
sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE " | |||
sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')' | |||
elsif field == 'watcher_id' | |||
db_table = Watcher.table_name | |||
db_field = 'user_id' | |||
sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " | |||
sql << sql_for_field(field, '=', v, db_table, db_field) + ')' | |||
else | |||
# regular field | |||
db_table = Issue.table_name | |||
db_field = field | |||
sql << '(' | |||
end | |||
# "me" value subsitution | |||
if %w(assigned_to_id author_id).include?(field) | |||
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") | |||
sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')' | |||
end | |||
case operator_for field | |||
when "=" | |||
sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" | |||
when "!" | |||
sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" | |||
when "!*" | |||
sql = sql + "#{db_table}.#{db_field} IS NULL" | |||
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter | |||
when "*" | |||
sql = sql + "#{db_table}.#{db_field} IS NOT NULL" | |||
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter | |||
when ">=" | |||
sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}" | |||
when "<=" | |||
sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}" | |||
when "o" | |||
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" | |||
when "c" | |||
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" | |||
when ">t-" | |||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)] | |||
when "<t-" | |||
sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time) | |||
when "t-" | |||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)] | |||
when ">t+" | |||
sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time) | |||
when "<t+" | |||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)] | |||
when "t+" | |||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)] | |||
when "t" | |||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)] | |||
when "w" | |||
from = l(:general_first_day_of_week) == '7' ? | |||
# week starts on sunday | |||
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) : | |||
# week starts on monday (Rails default) | |||
Time.now.at_beginning_of_week | |||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)] | |||
when "~" | |||
sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'" | |||
when "!~" | |||
sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'" | |||
end | |||
sql << ')' | |||
filters_clauses << sql | |||
end if filters and valid? | |||
(filters_clauses << project_statement).join(' AND ') | |||
@@ -363,6 +327,58 @@ class Query < ActiveRecord::Base | |||
private | |||
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ | |||
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) | |||
sql = '' | |||
case operator | |||
when "=" | |||
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" | |||
when "!" | |||
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" | |||
when "!*" | |||
sql = "#{db_table}.#{db_field} IS NULL" | |||
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter | |||
when "*" | |||
sql = "#{db_table}.#{db_field} IS NOT NULL" | |||
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter | |||
when ">=" | |||
sql = "#{db_table}.#{db_field} >= #{value.first.to_i}" | |||
when "<=" | |||
sql = "#{db_table}.#{db_field} <= #{value.first.to_i}" | |||
when "o" | |||
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" | |||
when "c" | |||
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" | |||
when ">t-" | |||
sql = date_range_clause(db_table, db_field, - value.first.to_i, 0) | |||
when "<t-" | |||
sql = date_range_clause(db_table, db_field, nil, - value.first.to_i) | |||
when "t-" | |||
sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i) | |||
when ">t+" | |||
sql = date_range_clause(db_table, db_field, value.first.to_i, nil) | |||
when "<t+" | |||
sql = date_range_clause(db_table, db_field, 0, value.first.to_i) | |||
when "t+" | |||
sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i) | |||
when "t" | |||
sql = date_range_clause(db_table, db_field, 0, 0) | |||
when "w" | |||
from = l(:general_first_day_of_week) == '7' ? | |||
# week starts on sunday | |||
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) : | |||
# week starts on monday (Rails default) | |||
Time.now.at_beginning_of_week | |||
sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)] | |||
when "~" | |||
sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'" | |||
when "!~" | |||
sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'" | |||
end | |||
return sql | |||
end | |||
def add_custom_fields_filters(custom_fields) | |||
@available_filters ||= {} | |||
@@ -382,4 +398,16 @@ class Query < ActiveRecord::Base | |||
@available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) | |||
end | |||
end | |||
# Returns a SQL clause for a date or datetime field. | |||
def date_range_clause(table, field, from, to) | |||
s = [] | |||
if from | |||
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)]) | |||
end | |||
if to | |||
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)]) | |||
end | |||
s.join(' AND ') | |||
end | |||
end |
@@ -78,11 +78,12 @@ class Repository < ActiveRecord::Base | |||
end | |||
# Default behaviour: we search in cached changesets | |||
def changesets_for_path(path) | |||
def changesets_for_path(path, options={}) | |||
path = "/#{path}" unless path.starts_with?('/') | |||
Change.find(:all, :include => :changeset, | |||
:conditions => ["repository_id = ? AND path = ?", id, path], | |||
:order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset) | |||
Change.find(:all, :include => {:changeset => :user}, | |||
:conditions => ["repository_id = ? AND path = ?", id, path], | |||
:order => "committed_on DESC, #{Changeset.table_name}.id DESC", | |||
:limit => options[:limit]).collect(&:changeset) | |||
end | |||
# Returns a path relative to the url of the repository | |||
@@ -98,6 +99,45 @@ class Repository < ActiveRecord::Base | |||
self.changesets.each(&:scan_comment_for_issue_ids) | |||
end | |||
# Returns an array of committers usernames and associated user_id | |||
def committers | |||
@committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}") | |||
end | |||
# Maps committers username to a user ids | |||
def committer_ids=(h) | |||
if h.is_a?(Hash) | |||
committers.each do |committer, user_id| | |||
new_user_id = h[committer] | |||
if new_user_id && (new_user_id.to_i != user_id.to_i) | |||
new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil) | |||
Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer]) | |||
end | |||
end | |||
@committers = nil | |||
true | |||
else | |||
false | |||
end | |||
end | |||
# Returns the Redmine User corresponding to the given +committer+ | |||
# It will return nil if the committer is not yet mapped and if no User | |||
# with the same username or email was found | |||
def find_committer_user(committer) | |||
if committer | |||
c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user) | |||
if c && c.user | |||
c.user | |||
elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/ | |||
username, email = $1.strip, $3 | |||
u = User.find_by_login(username) | |||
u ||= User.find_by_mail(email) unless email.blank? | |||
u | |||
end | |||
end | |||
end | |||
# fetch new changesets for all repositories | |||
# can be called periodically by an external script | |||
# eg. ruby script/runner "Repository.fetch_changesets" |
@@ -52,7 +52,7 @@ class Repository::Darcs < Repository | |||
end | |||
def cat(path, identifier=nil) | |||
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier) | |||
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier.to_s) | |||
scm.cat(path, patch.nil? ? nil : patch.scmid) | |||
end | |||
@@ -40,10 +40,11 @@ class Repository::Git < Repository | |||
'Git' | |||
end | |||
def changesets_for_path(path) | |||
Change.find(:all, :include => :changeset, | |||
def changesets_for_path(path, options={}) | |||
Change.find(:all, :include => {:changeset => :user}, | |||
:conditions => ["repository_id = ? AND path = ?", id, path], | |||
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset) | |||
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC", | |||
:limit => options[:limit]).collect(&:changeset) | |||
end | |||
def fetch_changesets | |||
@@ -58,20 +59,22 @@ class Repository::Git < Repository | |||
unless changesets.find_by_scmid(scm_revision) | |||
scm.revisions('', db_revision, nil, :reverse => true) do |revision| | |||
transaction do | |||
changeset = Changeset.create(:repository => self, | |||
:revision => revision.identifier, | |||
:scmid => revision.scmid, | |||
:committer => revision.author, | |||
:committed_on => revision.time, | |||
:comments => revision.message) | |||
revision.paths.each do |change| | |||
Change.create(:changeset => changeset, | |||
:action => change[:action], | |||
:path => change[:path], | |||
:from_path => change[:from_path], | |||
:from_revision => change[:from_revision]) | |||
if changesets.find_by_scmid(revision.scmid.to_s).nil? | |||
transaction do | |||
changeset = Changeset.create!(:repository => self, | |||
:revision => revision.identifier, | |||
:scmid => revision.scmid, | |||
:committer => revision.author, | |||
:committed_on => revision.time, | |||
:comments => revision.message) | |||
revision.paths.each do |change| | |||
Change.create!(:changeset => changeset, | |||
:action => change[:action], | |||
:path => change[:path], | |||
:from_path => change[:from_path], | |||
:from_revision => change[:from_revision]) | |||
end | |||
end | |||
end | |||
end |
@@ -40,9 +40,9 @@ class Repository::Subversion < Repository | |||
'Subversion' | |||
end | |||
def changesets_for_path(path) | |||
revisions = scm.revisions(path) | |||
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : [] | |||
def changesets_for_path(path, options={}) | |||
revisions = scm.revisions(path, nil, nil, :limit => options[:limit]) | |||
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : [] | |||
end | |||
# Returns a path relative to the url of the repository |
@@ -31,9 +31,9 @@ class Role < ActiveRecord::Base | |||
raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role) | |||
raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record? | |||
clear | |||
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" + | |||
connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" + | |||
" SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" + | |||
" FROM workflows" + | |||
" FROM #{Workflow.table_name}" + | |||
" WHERE role_id = #{role.id}" | |||
end | |||
end |
@@ -75,9 +75,9 @@ class Setting < ActiveRecord::Base | |||
cattr_accessor :available_settings | |||
@@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml")) | |||
Redmine::Plugin.registered_plugins.each do |id, plugin| | |||
Redmine::Plugin.all.each do |plugin| | |||
next unless plugin.settings | |||
@@available_settings["plugin_#{id}"] = {'default' => plugin.settings[:default], 'serialized' => true} | |||
@@available_settings["plugin_#{plugin.id}"] = {'default' => plugin.settings[:default], 'serialized' => true} | |||
end | |||
validates_uniqueness_of :name | |||
@@ -140,6 +140,10 @@ class Setting < ActiveRecord::Base | |||
per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort | |||
end | |||
def self.openid? | |||
Object.const_defined?(:OpenID) && self['openid'].to_s == '1' | |||
end | |||
# Checks if settings have changed since the values were read | |||
# and clears the cache hash if it's the case | |||
# Called once per request |
@@ -32,7 +32,7 @@ class TimeEntry < ActiveRecord::Base | |||
:description => :comments | |||
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on | |||
validates_numericality_of :hours, :allow_nil => true | |||
validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid | |||
validates_length_of :comments, :maximum => 255, :allow_nil => true | |||
def after_initialize | |||
@@ -54,7 +54,7 @@ class TimeEntry < ActiveRecord::Base | |||
end | |||
def hours=(h) | |||
write_attribute :hours, (h.is_a?(String) ? h.to_hours : h) | |||
write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h) | |||
end | |||
# tyear, tmonth, tweek assigned where setting spent_on attributes |
@@ -23,9 +23,9 @@ class Tracker < ActiveRecord::Base | |||
raise "Can not copy workflow from a #{tracker.class}" unless tracker.is_a?(Tracker) | |||
raise "Can not copy workflow from/to an unsaved tracker" if proxy_owner.new_record? || tracker.new_record? | |||
clear | |||
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" + | |||
connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" + | |||
" SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" + | |||
" FROM workflows" + | |||
" FROM #{Workflow.table_name}" + | |||
" WHERE tracker_id = #{tracker.id}" | |||
end | |||
end |
@@ -37,10 +37,14 @@ class User < ActiveRecord::Base | |||
has_many :members, :dependent => :delete_all | |||
has_many :projects, :through => :memberships | |||
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify | |||
has_many :changesets, :dependent => :nullify | |||
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' | |||
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'" | |||
belongs_to :auth_source | |||
# Active non-anonymous users scope | |||
named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}" | |||
acts_as_customizable | |||
attr_accessor :password, :password_confirmation | |||
@@ -50,7 +54,7 @@ class User < ActiveRecord::Base | |||
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } | |||
validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? } | |||
validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? } | |||
validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false | |||
# Login must contain lettres, numbers, underscores only | |||
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i | |||
validates_length_of :login, :maximum => 30 | |||
@@ -70,17 +74,19 @@ class User < ActiveRecord::Base | |||
# update hashed_password if password was set | |||
self.hashed_password = User.hash_password(self.password) if self.password | |||
end | |||
def self.active | |||
with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do | |||
yield | |||
end | |||
def reload(*args) | |||
@name = nil | |||
super | |||
end | |||
def self.find_active(*args) | |||
active do | |||
find(*args) | |||
def identity_url=(url) | |||
begin | |||
self.write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url)) | |||
rescue InvalidOpenId | |||
# Invlaid url, don't save | |||
end | |||
self.read_attribute(:identity_url) | |||
end | |||
# Returns the user that matches provided login and password, or nil | |||
@@ -119,8 +125,11 @@ class User < ActiveRecord::Base | |||
# Return user's full name for display | |||
def name(formatter = nil) | |||
f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname] | |||
eval '"' + f + '"' | |||
if formatter | |||
eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"') | |||
else | |||
@name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"') | |||
end | |||
end | |||
def active? | |||
@@ -138,13 +147,25 @@ class User < ActiveRecord::Base | |||
def check_password?(clear_password) | |||
User.hash_password(clear_password) == self.hashed_password | |||
end | |||
# Generate and set a random password. Useful for automated user creation | |||
# Based on Token#generate_token_value | |||
# | |||
def random_password | |||
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a | |||
password = '' | |||
40.times { |i| password << chars[rand(chars.size-1)] } | |||
self.password = password | |||
self.password_confirmation = password | |||
self | |||
end | |||
def pref | |||
self.preference ||= UserPreference.new(:user => self) | |||
end | |||
def time_zone | |||
@time_zone ||= (self.pref.time_zone.blank? ? nil : TimeZone[self.pref.time_zone]) | |||
@time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone]) | |||
end | |||
def wants_comments_in_reverse_order? | |||
@@ -178,15 +199,15 @@ class User < ActiveRecord::Base | |||
token = Token.find_by_action_and_value('autologin', key) | |||
token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil | |||
end | |||
# Makes find_by_mail case-insensitive | |||
def self.find_by_mail(mail) | |||
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase]) | |||
end | |||
# Sort users by their display names | |||
def <=>(user) | |||
if user.nil? | |||
-1 | |||
elsif lastname.to_s.downcase == user.lastname.to_s.downcase | |||
firstname.to_s.downcase <=> user.firstname.to_s.downcase | |||
else | |||
lastname.to_s.downcase <=> user.lastname.to_s.downcase | |||
end | |||
self.to_s.downcase <=> user.to_s.downcase | |||
end | |||
def to_s |
@@ -19,7 +19,8 @@ class Version < ActiveRecord::Base | |||
before_destroy :check_integrity | |||
belongs_to :project | |||
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id' | |||
has_many :attachments, :as => :container, :dependent => :destroy | |||
acts_as_attachable :view_permission => :view_files, | |||
:delete_permission => :manage_files | |||
validates_presence_of :name | |||
validates_uniqueness_of :name, :scope => [:project_id] | |||
@@ -50,20 +51,20 @@ class Version < ActiveRecord::Base | |||
end | |||
def completed_pourcent | |||
if fixed_issues.count == 0 | |||
if issues_count == 0 | |||
0 | |||
elsif open_issues_count == 0 | |||
100 | |||
else | |||
(closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count | |||
issues_progress(false) + issues_progress(true) | |||
end | |||
end | |||
def closed_pourcent | |||
if fixed_issues.count == 0 | |||
if issues_count == 0 | |||
0 | |||
else | |||
closed_issues_count * 100.0 / fixed_issues.count | |||
issues_progress(false) | |||
end | |||
end | |||
@@ -72,6 +73,11 @@ class Version < ActiveRecord::Base | |||
effective_date && (effective_date < Date.today) && (open_issues_count > 0) | |||
end | |||
# Returns assigned issues count | |||
def issues_count | |||
@issue_count ||= fixed_issues.count | |||
end | |||
def open_issues_count | |||
@open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status) | |||
end | |||
@@ -103,4 +109,35 @@ private | |||
def check_integrity | |||
raise "Can't delete version" if self.fixed_issues.find(:first) | |||
end | |||
# Returns the average estimated time of assigned issues | |||
# or 1 if no issue has an estimated time | |||
# Used to weigth unestimated issues in progress calculation | |||
def estimated_average | |||
if @estimated_average.nil? | |||
average = fixed_issues.average(:estimated_hours).to_f | |||
if average == 0 | |||
average = 1 | |||
end | |||
@estimated_average = average | |||
end | |||
@estimated_average | |||
end | |||
# Returns the total progress of open or closed issues | |||
def issues_progress(open) | |||
@issues_progress ||= {} | |||
@issues_progress[open] ||= begin | |||
progress = 0 | |||
if issues_count > 0 | |||
ratio = open ? 'done_ratio' : 100 | |||
done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}", | |||
:include => :status, | |||
:conditions => ["is_closed = ?", !open]).to_f | |||
progress = done / (estimated_average * issues_count) | |||
end | |||
progress | |||
end | |||
end | |||
end |
@@ -43,6 +43,25 @@ class Wiki < ActiveRecord::Base | |||
page | |||
end | |||
# Finds a page by title | |||
# The given string can be of one of the forms: "title" or "project:title" | |||
# Examples: | |||
# Wiki.find_page("bar", project => foo) | |||
# Wiki.find_page("foo:bar") | |||
def self.find_page(title, options = {}) | |||
project = options[:project] | |||
if title.to_s =~ %r{^([^\:]+)\:(.*)$} | |||
project_identifier, title = $1, $2 | |||
project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier) | |||
end | |||
if project && project.wiki | |||
page = project.wiki.find_page(title) | |||
if page && page.content | |||
page | |||
end | |||
end | |||
end | |||
# turn a string into a valid page title | |||
def self.titleize(title) | |||
# replace spaces with _ and remove unwanted caracters |
@@ -37,6 +37,7 @@ class WikiContent < ActiveRecord::Base | |||
acts_as_activity_provider :type => 'wiki_edits', | |||
:timestamp => "#{WikiContent.versioned_table_name}.updated_on", | |||
:author_key => "#{WikiContent.versioned_table_name}.author_id", | |||
:permission => :view_wiki_edits, | |||
:find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + | |||
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " + |
@@ -21,7 +21,7 @@ require 'enumerator' | |||
class WikiPage < ActiveRecord::Base | |||
belongs_to :wiki | |||
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy | |||
has_many :attachments, :as => :container, :dependent => :destroy | |||
acts_as_attachable :delete_permission => :delete_wiki_pages_attachments | |||
acts_as_tree :order => 'title' | |||
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"}, | |||
@@ -111,6 +111,10 @@ class WikiPage < ActiveRecord::Base | |||
def editable_by?(usr) | |||
!protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project) | |||
end | |||
def attachments_deletable?(usr=User.current) | |||
editable_by?(usr) && super(usr) | |||
end | |||
def parent_title | |||
@parent_title || (self.parent && self.parent.pretty_title) |
@@ -21,4 +21,23 @@ class Workflow < ActiveRecord::Base | |||
belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id' | |||
validates_presence_of :role, :old_status, :new_status | |||
# Returns workflow transitions count by tracker and role | |||
def self.count_by_tracker_and_role | |||
counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{Workflow.table_name} GROUP BY role_id, tracker_id") | |||
roles = Role.find(:all, :order => 'builtin, position') | |||
trackers = Tracker.find(:all, :order => 'position') | |||
result = [] | |||
trackers.each do |tracker| | |||
t = [] | |||
roles.each do |role| | |||
row = counts.detect {|c| c['role_id'] == role.id.to_s && c['tracker_id'] == tracker.id.to_s} | |||
t << [role, (row.nil? ? 0 : row['c'].to_i)] | |||
end | |||
result << [tracker, t] | |||
end | |||
result | |||
end | |||
end |
@@ -10,6 +10,12 @@ | |||
<td align="right"><label for="password"><%=l(:field_password)%>:</label></td> | |||
<td align="left"><%= password_field_tag 'password', nil, :size => 40 %></td> | |||
</tr> | |||
<% if Setting.openid? %> | |||
<tr> | |||
<td align="right"><label for="openid_url"><%=l(:field_identity_url)%></label></td> | |||
<td align="left"><%= text_field_tag "openid_url" %></td> | |||
</tr> | |||
<% end %> | |||
<tr> | |||
<td></td> | |||
<td align="left"> |
@@ -1,4 +1,4 @@ | |||
<h2><%=l(:label_register)%></h2> | |||
<h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2> | |||
<% form_tag({:action => 'register'}, :class => "tabular") do %> | |||
<%= error_messages_for 'user' %> | |||
@@ -29,7 +29,12 @@ | |||
<p><label for="user_language"><%=l(:field_language)%></label> | |||
<%= select("user", "language", lang_options_for_select) %></p> | |||
<% @user.custom_field_values.each do |value| %> | |||
<% if Setting.openid? %> | |||
<p><label for="user_identity_url"><%=l(:field_identity_url)%></label> | |||
<%= text_field 'user', 'identity_url' %></p> | |||
<% end %> | |||
<% @user.custom_field_values.select {|v| v.editable? || v.required?}.each do |value| %> | |||
<p><%= custom_field_tag_with_label :user, value %></p> | |||
<% end %> | |||
<!--[eoform:user]--> |
@@ -2,19 +2,23 @@ | |||
<%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %> | |||
</div> | |||
<h2><%=h @user.name %></h2> | |||
<h2><%= avatar @user %> <%=h @user.name %></h2> | |||
<p> | |||
<%= mail_to(h(@user.mail)) unless @user.pref.hide_mail %> | |||
<div class="splitcontentleft"> | |||
<ul> | |||
<li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li> | |||
<% for custom_value in @custom_values %> | |||
<% if !custom_value.value.empty? %> | |||
<% unless @user.pref.hide_mail %> | |||
<li><%=l(:field_mail)%>: <%= mail_to(h(@user.mail), nil, :encode => 'javascript') %></li> | |||
<% end %> | |||
<% for custom_value in @custom_values %> | |||
<% if !custom_value.value.empty? %> | |||
<li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li> | |||
<% end %> | |||
<% end %> | |||
<% end %> | |||
<% end %> | |||
<li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li> | |||
<% unless @user.last_login_on.nil? %> | |||
<li><%=l(:field_last_login_on)%>: <%= format_date(@user.last_login_on) %></li> | |||
<% end %> | |||
</ul> | |||
</p> | |||
<% unless @memberships.empty? %> | |||
<h3><%=l(:label_project_plural)%></h3> | |||
@@ -25,8 +29,40 @@ | |||
<% end %> | |||
</ul> | |||
<% end %> | |||
</div> | |||
<div class="splitcontentright"> | |||
<% unless @events_by_day.empty? %> | |||
<h3><%= link_to l(:label_activity), :controller => 'projects', :action => 'activity', :user_id => @user, :from => @events_by_day.keys.first %></h3> | |||
<h3><%=l(:label_activity)%></h3> | |||
<p> | |||
<%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %> | |||
</p> | |||
</p> | |||
<div id="activity"> | |||
<% @events_by_day.keys.sort.reverse.each do |day| %> | |||
<h4><%= format_activity_day(day) %></h4> | |||
<dl> | |||
<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> | |||
<dt class="<%= e.event_type %>"> | |||
<span class="time"><%= format_time(e.event_datetime, false) %></span> | |||
<%= content_tag('span', h(e.project), :class => 'project') %> | |||
<%= link_to format_activity_title(e.event_title), e.event_url %></dt> | |||
<dd><span class="description"><%= format_activity_description(e.event_description) %></span></dd> | |||
<% end -%> | |||
</dl> | |||
<% end -%> | |||
</div> | |||
<% other_formats_links do |f| %> | |||
<%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => nil, :user_id => @user, :key => User.current.rss_key} %> | |||
<% end %> | |||
<% content_for :header_tags do %> | |||
<%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :user_id => @user, :format => :atom, :key => User.current.rss_key) %> | |||
<% end %> | |||
<% end %> | |||
</div> | |||
<% html_title @user.name %> |
@@ -19,7 +19,7 @@ | |||
<p class="icon22 icon22-tracker"> | |||
<%= link_to l(:label_tracker_plural), :controller => 'trackers' %> | | |||
<%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses' %> | | |||
<%= link_to l(:label_workflow), :controller => 'roles', :action => 'workflow' %> | |||
<%= link_to l(:label_workflow), :controller => 'workflows', :action => 'edit' %> | |||
</p> | |||
<p class="icon22 icon22-workflow"> | |||
@@ -34,6 +34,16 @@ | |||
<%= link_to l(:label_settings), :controller => 'settings' %> | |||
</p> | |||
<% menu_items_for(:admin_menu) do |item, caption, url, selected| -%> | |||
<%= content_tag 'p', | |||
link_to(h(caption), item.url, item.html_options), | |||
:class => ["icon22", "icon22-#{item.name}"].join(' ') %> | |||
<% end -%> | |||
<p class="icon22 icon22-plugin"> | |||
<%= link_to l(:label_plugins), :controller => 'admin', :action => 'plugins' %> | |||
</p> | |||
<p class="icon22 icon22-info"> | |||
<%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %> | |||
</p> |
@@ -4,24 +4,9 @@ | |||
<table class="list"> | |||
<tr class="odd"><td><%= l(:text_default_administrator_account_changed) %></td><td><%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr> | |||
<tr class="even"><td><%= l(:text_file_repository_writable) %></td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr> | |||
<tr class="even"><td><%= l(:text_file_repository_writable) %> (<%= Attachment.storage_path %>)</td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr> | |||
<tr class="even"><td><%= l(:text_plugin_assets_writable) %> (<%= Engines.public_directory %>)</td><td><%= image_tag (@flags[:plugin_assets_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr> | |||
<tr class="odd"><td><%= l(:text_rmagick_available) %></td><td><%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr> | |||
</table> | |||
<% if @plugins.any? %> | |||
| |||
<h3 class="icon22 icon22-plugin"><%= l(:label_plugins) %></h3> | |||
<table class="list"> | |||
<% @plugins.keys.sort {|x,y| x.to_s <=> y.to_s}.each do |plugin| %> | |||
<tr class="<%= cycle('odd', 'even') %>"> | |||
<td><%=h @plugins[plugin].name %></td> | |||
<td><%=h @plugins[plugin].description %></td> | |||
<td><%=h @plugins[plugin].author %></td> | |||
<td><%=h @plugins[plugin].version %></td> | |||
<td><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.to_s) if @plugins[plugin].configurable? %></td> | |||
</tr> | |||
<% end %> | |||
</table> | |||
<% end %> | |||
<% html_title(l(:label_information_plural)) -%> |
@@ -0,0 +1,19 @@ | |||
<h2><%= l(:label_plugins) %></h2> | |||
<% if @plugins.any? %> | |||
<table class="list plugins"> | |||
<% @plugins.each do |plugin| %> | |||
<tr class="<%= cycle('odd', 'even') %>"> | |||
<td><span class="name"><%=h plugin.name %></span> | |||
<%= content_tag('span', h(plugin.description), :class => 'description') unless plugin.description.blank? %> | |||
<%= content_tag('span', link_to(h(plugin.url), plugin.url), :class => 'url') unless plugin.url.blank? %> | |||
</td> | |||
<td class="author"><%= plugin.author_url.blank? ? h(plugin.author) : link_to(h(plugin.author), plugin.author_url) %></td> | |||
<td class="version"><%=h plugin.version %></td> | |||
<td class="configure"><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.id) if plugin.configurable? %></td> | |||
</tr> | |||
<% end %> | |||
</table> | |||
<% else %> | |||
<p class="nodata"><%= l(:label_no_data) %></p> | |||
<% end %> |
@@ -4,33 +4,33 @@ | |||
<h2><%=l(:label_project_plural)%></h2> | |||
<% form_tag() do %> | |||
<% form_tag({}, :method => :get) do %> | |||
<fieldset><legend><%= l(:label_filter_plural) %></legend> | |||
<label><%= l(:field_status) %> :</label> | |||
<%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> | |||
<%= submit_tag l(:button_apply), :class => "small" %> | |||
<label><%= l(:label_project) %>:</label> | |||
<%= text_field_tag 'name', params[:name], :size => 30 %> | |||
<%= submit_tag l(:button_apply), :class => "small", :name => nil %> | |||
</fieldset> | |||
<% end %> | |||
| |||
<table class="list"> | |||
<thead><tr> | |||
<%= sort_header_tag('name', :caption => l(:label_project)) %> | |||
<th><%=l(:label_project)%></th> | |||
<th><%=l(:field_description)%></th> | |||
<th><%=l(:label_subproject_plural)%></th> | |||
<%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %> | |||
<%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> | |||
<th><%=l(:field_is_public)%></th> | |||
<th><%=l(:field_created_on)%></th> | |||
<th></th> | |||
<th></th> | |||
</tr></thead> | |||
<tbody> | |||
<% for project in @projects %> | |||
<tr class="<%= cycle("odd", "even") %>"> | |||
<td><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> | |||
<td><%= textilizable project.short_description, :project => project %> | |||
<td align="center"><%= project.children.size %> | |||
<td align="center"><%= image_tag 'true.png' if project.is_public? %> | |||
<td align="center"><%= format_date(project.created_on) %> | |||
<tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %>"> | |||
<td class="name" style="padding-left: <%= project.level %>em;"><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %></td> | |||
<td><%= textilizable project.short_description, :project => project %></td> | |||
<td align="center"><%= image_tag 'true.png' if project.is_public? %></td> | |||
<td align="center"><%= format_date(project.created_on) %></td> | |||
<td align="center" style="width:10%"> | |||
<small> | |||
<%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %> | |||
@@ -45,6 +45,4 @@ | |||
</tbody> | |||
</table> | |||
<p class="pagination"><%= pagination_links_full @project_pages, @project_count %></p> | |||
<% html_title(l(:label_project_plural)) -%> |
@@ -3,14 +3,14 @@ | |||
<p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%> | |||
<%= h(" - #{attachment.description}") unless attachment.description.blank? %> | |||
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span> | |||
<% if options[:delete_url] %> | |||
<%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}), | |||
<% if options[:deletable] %> | |||
<%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => attachment}, | |||
:confirm => l(:text_are_you_sure), | |||
:method => :post, | |||
:class => 'delete', | |||
:title => l(:button_delete) %> | |||
<% end %> | |||
<% unless options[:no_author] %> | |||
<% if options[:author] %> | |||
<span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span> | |||
<% end %> | |||
</p> |
@@ -9,6 +9,7 @@ | |||
<th><%=l(:field_name)%></th> | |||
<th><%=l(:field_type)%></th> | |||
<th><%=l(:field_host)%></th> | |||
<th><%=l(:label_user_plural)%></th> | |||
<th></th> | |||
<th></th> | |||
</tr></thead> | |||
@@ -18,8 +19,12 @@ | |||
<td><%= link_to source.name, :action => 'edit', :id => source%></td> | |||
<td align="center"><%= source.auth_method_name %></td> | |||
<td align="center"><%= source.host %></td> | |||
<td align="center"><%= source.users.count %></td> | |||
<td align="center"><%= link_to l(:button_test), :action => 'test_connection', :id => source %></td> | |||
<td align="center"><%= button_to l(:button_delete), { :action => 'destroy', :id => source }, :confirm => l(:text_are_you_sure), :class => "button-small" %></td> | |||
<td align="center"><%= button_to l(:button_delete), { :action => 'destroy', :id => source }, | |||
:confirm => l(:text_are_you_sure), | |||
:class => "button-small", | |||
:disabled => source.users.any? %></td> | |||
</tr> | |||
<% end %> | |||
</tbody> |
@@ -29,11 +29,9 @@ | |||
</tbody> | |||
</table> | |||
<p class="other-formats"> | |||
<%= l(:label_export_to) %> | |||
<span><%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}, | |||
:class => 'feed' %></span> | |||
</p> | |||
<% other_formats_links do |f| %> | |||
<%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => @project, :show_messages => 1, :key => User.current.rss_key} %> | |||
<% end %> | |||
<% content_for :header_tags do %> | |||
<%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %> |
@@ -4,7 +4,7 @@ | |||
<%= link_to_if_authorized l(:label_message_new), | |||
{:controller => 'messages', :action => 'new', :board_id => @board}, | |||
:class => 'icon icon-add', | |||
:onclick => 'Element.show("add-message"); return false;' %> | |||
:onclick => 'Element.show("add-message"); Form.Element.focus("message_subject"); return false;' %> | |||
<%= watcher_tag(@board, User.current) %> | |||
</div> | |||
@@ -26,15 +26,16 @@ | |||
</div> | |||
<h2><%=h @board.name %></h2> | |||
<p class="subtitle"><%=h @board.description %></p> | |||
<% if @topics.any? %> | |||
<table class="list messages"> | |||
<thead><tr> | |||
<th><%= l(:field_subject) %></th> | |||
<th><%= l(:field_author) %></th> | |||
<%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %> | |||
<%= sort_header_tag("#{Message.table_name}.replies_count", :caption => l(:label_reply_plural)) %> | |||
<%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %> | |||
<%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> | |||
<%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %> | |||
<%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %> | |||
</tr></thead> | |||
<tbody> | |||
<% @topics.each do |topic| %> |
@@ -1,4 +1,5 @@ | |||
<% Redmine::UnifiedDiff.new(diff, diff_type).each do |table_file| -%> | |||
<% diff = Redmine::UnifiedDiff.new(diff, :type => diff_type, :max_lines => Setting.diff_max_lines_displayed.to_i) -%> | |||
<% diff.each do |table_file| -%> | |||
<div class="autoscroll"> | |||
<% if diff_type == 'sbs' -%> | |||
<table class="filecontent CodeRay"> | |||
@@ -62,3 +63,5 @@ | |||
</div> | |||
<% end -%> | |||
<%= l(:text_diff_truncated) if diff.truncated? %> |
@@ -6,7 +6,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do | |||
xml.id url_for(:controller => 'welcome', :only_path => false) | |||
xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema) | |||
xml.author { xml.name "#{Setting.app_title}" } | |||
xml.generator(:uri => Redmine::Info.url, :version => Redmine::VERSION) { xml.text! Redmine::Info.versioned_name; } | |||
xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; } | |||
@items.each do |item| | |||
xml.entry do | |||
url = url_for(item.event_url(:only_path => false)) |
@@ -49,23 +49,6 @@ function toggle_custom_field_format() { | |||
} | |||
} | |||
function addValueField() { | |||
var f = $$('p#custom_field_possible_values span'); | |||
p = document.getElementById("custom_field_possible_values"); | |||
var v = f[0].cloneNode(true); | |||
v.childNodes[0].value = ""; | |||
p.appendChild(v); | |||
} | |||
function deleteValueField(e) { | |||
var f = $$('p#custom_field_possible_values span'); | |||
if (f.length == 1) { | |||
e.parentNode.childNodes[0].value = ""; | |||
} else { | |||
Element.remove(e.parentNode); | |||
} | |||
} | |||
//]]> | |||
</script> | |||
@@ -76,22 +59,22 @@ function deleteValueField(e) { | |||
<%= f.text_field :min_length, :size => 5, :no_label => true %> - | |||
<%= f.text_field :max_length, :size => 5, :no_label => true %><br>(<%=l(:text_min_max_length_info)%>)</p> | |||
<p><%= f.text_field :regexp, :size => 50 %><br>(<%=l(:text_regexp_info)%>)</p> | |||
<p id="custom_field_possible_values"><label><%= l(:field_possible_values) %> <%= image_to_function "add.png", "addValueField();return false" %></label> | |||
<% (@custom_field.possible_values.to_a + [""]).each do |value| %> | |||
<span><%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %><br /></span> | |||
<% end %> | |||
</p> | |||
<p id="custom_field_possible_values"><%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"), | |||
:cols => 20, | |||
:rows => 15 %> | |||
<br /><em><%= l(:text_custom_field_possible_values_info) %></em></p> | |||
<p><%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %></p> | |||
</div> | |||
<div class="box"> | |||
<% case @custom_field.type.to_s | |||
<% case @custom_field.class.name | |||
when "IssueCustomField" %> | |||
<fieldset><legend><%=l(:label_tracker_plural)%></legend> | |||
<% for tracker in @trackers %> | |||
<%= check_box_tag "tracker_ids[]", tracker.id, (@custom_field.trackers.include? tracker) %> <%= tracker.name %> | |||
<%= check_box_tag "custom_field[tracker_ids][]", tracker.id, (@custom_field.trackers.include? tracker) %> <%= tracker.name %> | |||
<% end %> | |||
<%= hidden_field_tag "custom_field[tracker_ids][]", '' %> | |||
</fieldset> | |||
| |||
<p><%= f.check_box :is_required %></p> | |||
@@ -101,6 +84,7 @@ when "IssueCustomField" %> | |||
<% when "UserCustomField" %> | |||
<p><%= f.check_box :is_required %></p> | |||
<p><%= f.check_box :editable %></p> | |||
<% when "ProjectCustomField" %> | |||
<p><%= f.check_box :is_required %></p> |
@@ -2,7 +2,7 @@ | |||
<%= link_to_if_authorized l(:label_document_new), | |||
{:controller => 'documents', :action => 'new', :project_id => @project}, | |||
:class => 'icon icon-add', | |||
:onclick => 'Element.show("add-document"); return false;' %> | |||
:onclick => 'Element.show("add-document"); Form.Element.focus("document_title"); return false;' %> | |||
</div> | |||
<div id="add-document" style="display:none;"> |
@@ -12,7 +12,7 @@ | |||
</div> | |||
<h3><%= l(:label_attachment_plural) %></h3> | |||
<%= link_to_attachments @attachments, :delete_url => (authorize_for('documents', 'destroy_attachment') ? {:controller => 'documents', :action => 'destroy_attachment', :id => @document} : nil) %> | |||
<%= link_to_attachments @document %> | |||
<% if authorize_for('documents', 'add_attachment') %> | |||
<p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;", |
@@ -2,7 +2,7 @@ | |||
<div class="changeset <%= cycle('odd', 'even') %>"> | |||
<p><%= link_to("#{l(:label_revision)} #{changeset.revision}", | |||
:controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision) %><br /> | |||
<span class="author"><%= authoring(changeset.committed_on, changeset.committer) %></span></p> | |||
<span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p> | |||
<%= textilizable(changeset, :comments) %> | |||
</div> | |||
<% end %> |
@@ -35,6 +35,7 @@ | |||
<fieldset><legend><%= l(:field_notes) %></legend> | |||
<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> | |||
<%= wikitoolbar_for 'notes' %> | |||
<%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %> | |||
<p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p> | |||
</fieldset> |
@@ -8,13 +8,14 @@ | |||
<div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>> | |||
<p><%= f.text_field :subject, :size => 80, :required => true %></p> | |||
<p><%= f.text_area :description, :required => true, | |||
<p><%= f.text_area :description, | |||
:cols => 60, | |||
:rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), | |||
:accesskey => accesskey(:edit), | |||
:class => 'wiki-edit' %></p> | |||
</div> | |||
<div class="attributes"> | |||
<div class="splitcontentleft"> | |||
<% if @issue.new_record? || @allowed_statuses.any? %> | |||
<p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p> | |||
@@ -24,11 +25,13 @@ | |||
<p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p> | |||
<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p> | |||
<% unless @project.issue_categories.empty? %> | |||
<p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %> | |||
<%= prompt_to_remote(l(:label_issue_category_new), | |||
l(:label_issue_category_new), 'category[name]', | |||
{:controller => 'projects', :action => 'add_issue_category', :id => @project}, | |||
:class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p> | |||
<% end %> | |||
<%= content_tag('p', f.select(:fixed_version_id, | |||
(@project.versions.sort.collect {|v| [v.name, v.id]}), | |||
{ :include_blank => true })) unless @project.versions.empty? %> | |||
@@ -43,11 +46,20 @@ | |||
<div style="clear:both;"> </div> | |||
<%= render :partial => 'form_custom_fields' %> | |||
</div> | |||
<% if @issue.new_record? %> | |||
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p> | |||
<% end %> | |||
<% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%> | |||
<p><label><%= l(:label_issue_watchers) %></label> | |||
<% @issue.project.users.sort.each do |user| -%> | |||
<label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watcher_user_ids.include?(user.id) %> <%=h user %></label> | |||
<% end -%> | |||
</p> | |||
<% end %> | |||
<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %> | |||
<%= wikitoolbar_for 'issue_description' %> |
@@ -1,3 +1,4 @@ | |||
<div class="attributes"> | |||
<div class="splitcontentleft"> | |||
<p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p> | |||
<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p> | |||
@@ -8,3 +9,4 @@ | |||
(@project.versions.sort.collect {|v| [v.name, v.id]}), | |||
{ :include_blank => true })) unless @project.versions.empty? %> | |||
</div> | |||
</div> |
@@ -1,14 +1,16 @@ | |||
<% reply_links = authorize_for('issues', 'edit') -%> | |||
<% for journal in journals %> | |||
<div id="change-<%= journal.id %>" class="journal"> | |||
<h4><div style="float:right;"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div> | |||
<%= content_tag('a', '', :name => "note-#{journal.indice}")%> | |||
<%= format_time(journal.created_on) %> - <%= journal.user.name %></h4> | |||
<ul> | |||
<% for detail in journal.details %> | |||
<li><%= show_detail(detail) %></li> | |||
<% end %> | |||
</ul> | |||
<%= render_notes(journal, :reply_links => reply_links) unless journal.notes.blank? %> | |||
</div> | |||
<div id="change-<%= journal.id %>" class="journal"> | |||
<h4><div style="float:right;"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div> | |||
<%= content_tag('a', '', :name => "note-#{journal.indice}")%> | |||
<%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %></h4> | |||
<%= avatar(journal.user, :size => "32") %> | |||
<ul> | |||
<% for detail in journal.details %> | |||
<li><%= show_detail(detail) %></li> | |||
<% end %> | |||
</ul> | |||
<%= render_notes(journal, :reply_links => reply_links) unless journal.notes.blank? %> | |||
</div> | |||
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %> | |||
<% end %> |
@@ -4,14 +4,14 @@ | |||
<th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;', | |||
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> | |||
</th> | |||
<%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %> | |||
<%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> | |||
<% query.columns.each do |column| %> | |||
<%= column_header(column) %> | |||
<% end %> | |||
</tr></thead> | |||
<tbody> | |||
<% issues.each do |issue| -%> | |||
<tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>"> | |||
<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= css_issue_classes(issue) %>"> | |||
<td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td> | |||
<td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td> | |||
<% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> |
@@ -3,21 +3,22 @@ | |||
<table class="list issues"> | |||
<thead><tr> | |||
<th>#</th> | |||
<th><%=l(:field_project)%></th> | |||
<th><%=l(:field_tracker)%></th> | |||
<th><%=l(:field_subject)%></th> | |||
</tr></thead> | |||
<tbody> | |||
<% for issue in issues %> | |||
<tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>"> | |||
<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= css_issue_classes(issue) %>"> | |||
<td class="id"> | |||
<%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> | |||
<%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> | |||
<%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> | |||
</td> | |||
<td><%=h issue.project.name %> - <%= issue.tracker.name %><br /> | |||
<%= issue.status.name %> - <%= format_time(issue.updated_on) %></td> | |||
<td class="project"><%=h issue.project %></td> | |||
<td class="tracker"><%=h issue.tracker %></td> | |||
<td class="subject"> | |||
<%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %> | |||
</td> | |||
<%= link_to h(truncate(issue.subject, 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>) | |||
</td> | |||
</tr> | |||
<% end %> | |||
</tbody> |
@@ -8,9 +8,10 @@ | |||
<% if @issue.relations.any? %> | |||
<table style="width:100%"> | |||
<% @issue.relations.each do |relation| %> | |||
<% @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation| %> | |||
<tr> | |||
<td><%= l(relation.label_for(@issue)) %> <%= "(#{lwr(:actionview_datehelper_time_in_words_day, relation.delay)})" if relation.delay && relation.delay != 0 %> <%= link_to_issue relation.other_issue(@issue) %></td> | |||
<td><%= l(relation.label_for(@issue)) %> <%= "(#{lwr(:actionview_datehelper_time_in_words_day, relation.delay)})" if relation.delay && relation.delay != 0 %> | |||
<%= h(relation.other_issue(@issue).project) + ' - ' if Setting.cross_project_issue_relations? %> <%= link_to_issue relation.other_issue(@issue) %></td> | |||
<td><%=h relation.other_issue(@issue).subject %></td> | |||
<td><%= relation.other_issue(@issue).status.name %></td> | |||
<td><%= format_date(relation.other_issue(@issue).start_date) %></td> |
@@ -2,23 +2,25 @@ | |||
<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br /> | |||
<% if @project %> | |||
<%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br /> | |||
<%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %> | |||
<%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %><br /> | |||
<% end %> | |||
<%= call_hook(:view_issues_sidebar_issues_bottom) %> | |||
<% planning_links = [] | |||
planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :project_id => @project) | |||
planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :project_id => @project) | |||
planning_links.compact! | |||
unless planning_links.empty? %> | |||
planning_links << link_to(l(:label_calendar), :action => 'calendar', :project_id => @project) if User.current.allowed_to?(:view_calendar, @project, :global => true) | |||
planning_links << link_to(l(:label_gantt), :action => 'gantt', :project_id => @project) if User.current.allowed_to?(:view_gantt, @project, :global => true) | |||
%> | |||
<% unless planning_links.empty? %> | |||
<h3><%= l(:label_planning) %></h3> | |||
<p><%= planning_links.join(' | ') %></p> | |||
<% end %> | |||
<%= call_hook(:view_issues_sidebar_planning_bottom) %> | |||
<% end %> | |||
<% unless sidebar_queries.empty? -%> | |||
<h3><%= l(:label_query_plural) %></h3> | |||
<% sidebar_queries.each do |query| -%> | |||
<%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %><br /> | |||
<%= link_to(h(query.name), :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query) %><br /> | |||
<% end -%> | |||
<%= call_hook(:view_issues_sidebar_queries_bottom) %> | |||
<% end -%> |