r18658@gaspard (orig r1900): jplang | 2008-09-22 21:50:10 +0200
Truncate comments on changeset list. r18659@gaspard (orig r1901): jplang | 2008-09-23 19:03:51 +0200 Fixes html escaping. r18660@gaspard (orig r1902): winterheart | 2008-09-24 16:45:20 +0200 Patch #1938, update for nl.yml r18661@gaspard (orig r1903): jplang | 2008-09-24 19:30:36 +0200 Fixes back_url in login filter (#1900). r18662@gaspard (orig r1904): jplang | 2008-09-24 19:32:49 +0200 Reverts r1903. r18663@gaspard (orig r1905): jplang | 2008-09-24 19:33:02 +0200 Fixes back_url in login filter (#1900). r18667@gaspard (orig r1907): jplang | 2008-09-25 20:51:03 +0200 Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled. r18669@gaspard (orig r1909): winterheart | 2008-09-27 21:06:48 +0200 Fixed #1961, pt-br update r18670@gaspard (orig r1910): jplang | 2008-09-28 09:54:41 +0200 Fixed: Latest news appear on the homepage for projects with the News module disabled (#1941). r18671@gaspard (orig r1911): jplang | 2008-09-28 10:05:55 +0200 Fixed: the default status is lost when reordering issue statuses (#1955). r18672@gaspard (orig r1912): jplang | 2008-09-28 10:19:25 +0200 Wrap 'Assigned to' column on the issue list (#1960). r18673@gaspard (orig r1913): jplang | 2008-09-28 10:41:17 +0200 Fixed: Status list on bulk edit form does not follow normal sequence (#1956). r18674@gaspard (orig r1914): jplang | 2008-09-28 14:03:17 +0200 Adds a workflow overview screen. Workflow setup moved to a dedicated controller. r18675@gaspard (orig r1915): jplang | 2008-09-28 14:20:47 +0200 Fixes workflow setup link on trackers list (follows r1914). r18676@gaspard (orig r1916): jplang | 2008-09-28 14:36:30 +0200 Slight changes to the workflow setup screen. r18677@gaspard (orig r1917): jplang | 2008-09-28 15:10:00 +0200 Fixes Workflow.count_by_tracker_and_role. r18678@gaspard (orig r1918): edavis10 | 2008-09-30 01:55:11 +0200 Slight non-code change r18679@gaspard (orig r1919): edavis10 | 2008-09-30 01:56:35 +0200 Reverting slight non-code change r18680@gaspard (orig r1920): edavis10 | 2008-09-30 02:02:46 +0200 Slight non-code change to test git sync r18681@gaspard (orig r1921): edavis10 | 2008-09-30 07:18:50 +0200 Adds :view_layouts_base_body_bottom hook r18682@gaspard (orig r1922): edavis10 | 2008-10-02 04:40:29 +0200 Fixed a failing assertion in test_post_edit_with_attachment_only that would occur when running the full test suite but not the functional test suite. r18683@gaspard (orig r1923): edavis10 | 2008-10-02 05:23:35 +0200 Added tests to cover IssueStatus.destroy and IssueStatus.check_integrity r18684@gaspard (orig r1924): jplang | 2008-10-04 19:38:31 +0200 Escape image filename regexp (#1971). r18685@gaspard (orig r1925): winterheart | 2008-10-05 21:30:58 +0200 #1988, update for ko.yml r18686@gaspard (orig r1926): winterheart | 2008-10-05 22:40:25 +0200 Patch #1987, ca.yml update, thanks to Joan Duran for file r18687@gaspard (orig r1927): winterheart | 2008-10-06 17:00:56 +0200 #1992 update pt.yml, thanks to Pedro Araújo r18688@gaspard (orig r1928): winterheart | 2008-10-07 19:41:16 +0200 Patch #2001, update for Polish language r18689@gaspard (orig r1929): winterheart | 2008-10-11 13:32:30 +0200 Patch #2005, nl.yml update r18690@gaspard (orig r1930): jplang | 2008-10-12 21:13:36 +0200 Remove pre tag attributes. r18691@gaspard (orig r1931): nbc | 2008-10-16 00:30:57 +0200 bugfix to two failed tests r18692@gaspard (orig r1932): nbc | 2008-10-16 01:50:33 +0200 add plain text option for mail #2029 r18693@gaspard (orig r1933): jplang | 2008-10-16 21:13:43 +0200 Makes email address case-insensitive in MailHandler (#2032). r18694@gaspard (orig r1934): winterheart | 2008-10-16 22:50:50 +0200 #2036 update for hu.yml r18695@gaspard (orig r1935): winterheart | 2008-10-16 22:51:27 +0200 Update for ru.yml r18696@gaspard (orig r1936): edavis10 | 2008-10-18 01:30:37 +0200 Added a plugin hook :routes that plugins can use to add and even override routes r18697@gaspard (orig r1937): winterheart | 2008-10-18 12:03:50 +0200 #2043, #2044, #2046, translation updates r18698@gaspard (orig r1938): jplang | 2008-10-18 12:07:49 +0200 Adds 'Delete wiki pages attachments' permission. r18699@gaspard (orig r1939): jplang | 2008-10-18 12:18:21 +0200 Show the most recent file when displaying an inline image. r18700@gaspard (orig r1940): jplang | 2008-10-18 12:42:29 +0200 link_to project homepage instead of auto_link (#1937). r18701@gaspard (orig r1941): jplang | 2008-10-18 13:25:27 +0200 Fixed: textile footnotes no longer work after r1113 (#974). r18702@gaspard (orig r1942): winterheart | 2008-10-23 17:24:16 +0200 #1928 it.yml update r18703@gaspard (orig r1943): jplang | 2008-10-24 17:24:35 +0200 Makes permission screens localized (#2070). r18704@gaspard (orig r1944): jplang | 2008-10-24 17:39:40 +0200 AuthSource list: display associated users count and disable 'Delete' buton if any (#2041). r18705@gaspard (orig r1945): jplang | 2008-10-24 18:59:15 +0200 Adds the ability to search for a user on the administration users list. r18706@gaspard (orig r1946): jplang | 2008-10-24 19:01:42 +0200 Adds functional test for user search. r18707@gaspard (orig r1947): jplang | 2008-10-24 19:12:39 +0200 Adds the ability to search for a project name or identifier on the administration projects list. r18708@gaspard (orig r1948): edavis10 | 2008-10-25 06:21:57 +0200 Added hook :view_repositories_show_contextual to allow adding items to the repository's contextual menu. #2073 r18709@gaspard (orig r1949): edavis10 | 2008-10-25 06:37:31 +0200 Renamed the .rb files in the plugin_generator to end in .erb. The .rb was causing rdoc to try to document them and fail. * Updated the generator's manifest to use the new files * Renamed template README to README.rdoc #2011 r18710@gaspard (orig r1950): edavis10 | 2008-10-25 06:46:21 +0200 Added the board's description below the board's name. Thanks to Go MAEDA for the patch. #2079 r18711@gaspard (orig r1951): jplang | 2008-10-25 11:35:51 +0200 Renames template ruby files to erb. r18712@gaspard (orig r1952): jplang | 2008-10-25 11:55:31 +0200 Adds #delete_menu_item to the plugin API (#2087). r18713@gaspard (orig r1953): jplang | 2008-10-25 12:23:29 +0200 Check that git changeset is not in the database before creating it (#1419). r18714@gaspard (orig r1954): jplang | 2008-10-26 16:17:26 +0100 Slight change to english string (#2088). r18715@gaspard (orig r1955): jplang | 2008-10-27 12:08:29 +0100 Makes wiki text formatter pluggable. Original patch #2025 by Yuki Sonoda slightly edited. r18716@gaspard (orig r1956): jplang | 2008-10-27 12:50:23 +0100 Adds back textile acronyms support (#2077). r18717@gaspard (orig r1957): jplang | 2008-10-27 13:34:01 +0100 Makes GLoc language global. r18718@gaspard (orig r1958): jplang | 2008-10-28 11:43:34 +0100 Fixed: Inline images don't work if file name has upper case letters or if image is in BMP format (#2102). r18719@gaspard (orig r1959): winterheart | 2008-10-28 17:08:19 +0100 #2080, #2097, #2100 - ja, zh-tw, zh updates r18720@gaspard (orig r1960): edavis10 | 2008-10-28 21:29:38 +0100 Added :view_timelog_edit_form_bottom hook to the timelog/edit form. r18721@gaspard (orig r1961): winterheart | 2008-10-29 00:31:14 +0100 Update for ru.yml r18722@gaspard (orig r1962): edavis10 | 2008-10-30 03:58:04 +0100 Gravatar support for issue detai, user grid, and activity stream r18723@gaspard (orig r1963): edavis10 | 2008-10-30 03:58:10 +0100 styling tweaks for gravatars r18724@gaspard (orig r1964): edavis10 | 2008-10-30 03:58:16 +0100 styling tweaks for gravatars r18725@gaspard (orig r1965): edavis10 | 2008-10-30 03:58:23 +0100 Reduced the size of the gravatar on the issue history r18726@gaspard (orig r1966): edavis10 | 2008-10-30 03:58:28 +0100 Fixed a bug with using gravatar on a nil value. r18727@gaspard (orig r1967): edavis10 | 2008-10-30 03:58:34 +0100 Added gravatar image to the user's public account page r18728@gaspard (orig r1968): edavis10 | 2008-10-30 04:29:30 +0100 Fixed typo in an English string, 'View calender' r18729@gaspard (orig r1969): edavis10 | 2008-10-30 04:49:04 +0100 Link the version name to VersionsController#show in the issue list. r18730@gaspard (orig r1970): edavis10 | 2008-10-31 01:09:36 +0100 Tweaking of the CSS for the gravatars. #1776 r18731@gaspard (orig r1971): edavis10 | 2008-10-31 01:19:48 +0100 Tighened up the gravator CSS in the issue div r18732@gaspard (orig r1972): edavis10 | 2008-10-31 01:41:28 +0100 Added an option to turn user Gravatars on or off * Option can be found in Administration > General, called "Use Gravatar user icons" * Defaulting Gravatars to off * Added a helper gravatar_for_mail to check the setting before rendering the Gravatar. #1776 r18733@gaspard (orig r1973): winterheart | 2008-10-31 15:38:09 +0100 Populating new string with rake gloc:update r18734@gaspard (orig r1974): winterheart | 2008-10-31 15:48:07 +0100 Update pt-rb, #2105 r18735@gaspard (orig r1975): winterheart | 2008-10-31 15:49:33 +0100 Update zh-tw, #2116 r18736@gaspard (orig r1976): winterheart | 2008-10-31 15:58:05 +0100 update ru.yml r18737@gaspard (orig r1977): winterheart | 2008-11-01 17:42:49 +0100 #2121, pt-br update r18738@gaspard (orig r1978): edavis10 | 2008-11-04 19:27:13 +0100 Added :view_projects_form plugin hook r18739@gaspard (orig r1979): edavis10 | 2008-11-06 06:37:29 +0100 Included Redmine::Hook::Helper to ActionController::Base so call_hook is available in all controllers. #2111 r18740@gaspard (orig r1980): winterheart | 2008-11-07 11:41:10 +0100 #2127, #2129, #2130, #2135, translation updates. Thanks to all participants :) r18741@gaspard (orig r1981): winterheart | 2008-11-07 11:53:09 +0100 Intial support Vietnamese language (#2125), thanks to Kỳ Anh Huỳnh for work r18742@gaspard (orig r1982): winterheart | 2008-11-07 12:07:25 +0100 Ooops, wrong. r18743@gaspard (orig r1983): winterheart | 2008-11-07 12:12:12 +0100 Intial support Vietnamese language (#2125), thanks to Kỳ Anh Huỳnh for work (now - really) r18744@gaspard (orig r1984): winterheart | 2008-11-07 12:20:25 +0100 refreshing vn.yml (#2125) r18745@gaspard (orig r1985): winterheart | 2008-11-07 12:22:26 +0100 D'oh... r18746@gaspard (orig r1986): winterheart | 2008-11-07 12:28:29 +0100 Update for pl.yml, #1299 r18747@gaspard (orig r1987): jplang | 2008-11-07 14:08:01 +0100 French translation update. r18748@gaspard (orig r1988): jplang | 2008-11-07 15:35:18 +0100 Email address should be lowercased for gravatar (#2145). r18749@gaspard (orig r1989): jplang | 2008-11-07 16:37:17 +0100 Host setting should contain the path prefix (Redmine base URL) to properly generate links in emails that are sent offline (#2122). r18750@gaspard (orig r1990): jplang | 2008-11-07 18:27:56 +0100 Fixed: broken subject when submitting issue via email written in japanese (Patch #2059 by Go MAEDA). r18751@gaspard (orig r1991): edavis10 | 2008-11-08 01:12:43 +0100 Removing the custom Redmine hook in routes in favor of Engine's hook. * Plugins' routes.rb are now added automatically to Redmine's routing, including the ability to override Redmine's default routing. Thank you to Jean-Baptiste Barth for the suggestion. #2142 r18752@gaspard (orig r1992): jplang | 2008-11-08 14:25:45 +0100 Do not use @:skip_relative_url_root@ to generate urls in Mailer (#2122). r18753@gaspard (orig r1993): jplang | 2008-11-08 16:18:02 +0100 Fixes syntax highlighting broken by r1930 (#2143). r18754@gaspard (orig r1994): jplang | 2008-11-08 16:28:00 +0100 Fixed Bazaar shared repository browsing (#2101, patch #1685 by Dmitry Shaposhnik). r18755@gaspard (orig r1995): jplang | 2008-11-08 16:50:51 +0100 Tells git to output dates in ISO format. Fixes: Git Adapter date parsing ignores timezone (#2149). r18756@gaspard (orig r1996): jplang | 2008-11-08 18:15:18 +0100 git path reverted. r18757@gaspard (orig r1997): winterheart | 2008-11-08 23:34:41 +0100 #2126, initial support of Slovak, thank to Stanislav Pach for translation r18758@gaspard (orig r1998): winterheart | 2008-11-09 01:29:20 +0100 populating new string, updates for ru.yml and sv.yml (#2126) r18759@gaspard (orig r1999): jplang | 2008-11-09 13:07:35 +0100 Git adapter: use commit time instead of author time (#2108). r18760@gaspard (orig r2000): jplang | 2008-11-09 15:52:16 +0100 Changes ApplicationHelper#gravatar_for_mail to #avatar that takes a User or a String (less code in views). r18761@gaspard (orig r2001): jplang | 2008-11-09 18:53:30 +0100 Fixes activity date param. r18762@gaspard (orig r2002): jplang | 2008-11-09 18:56:20 +0100 Link to activity view when displaying dates. r18763@gaspard (orig r2003): jplang | 2008-11-09 21:39:49 +0100 Hide Redmine version in atom feeds and pdf properties (#794). r18764@gaspard (orig r2004): jplang | 2008-11-10 12:33:04 +0100 Fixed: non-ASCII subversion path can't be displayed (patch #1993 by Chaoqun Zou). r18765@gaspard (orig r2005): jplang | 2008-11-10 13:23:54 +0100 Include GLoc in hook listener base class (#2112). r18766@gaspard (orig r2006): jplang | 2008-11-10 19:59:06 +0100 Maps repository users to Redmine users (#1383). Users with same username or email are automatically mapped. Mapping can be manually adjusted in repository settings. Multiple usernames can be mapped to the same Redmine user. r18767@gaspard (orig r2007): jplang | 2008-11-10 20:09:00 +0100 Eager-load users. r18768@gaspard (orig r2008): jplang | 2008-11-11 13:07:03 +0100 Fixes a typo in en.yml. r18769@gaspard (orig r2009): jplang | 2008-11-11 13:50:11 +0100 Eager-load users. r18770@gaspard (orig r2010): jplang | 2008-11-11 13:59:28 +0100 Sort users by their display names so that user dropdown lists are sorted alphabetically (#2015). r18771@gaspard (orig r2011): jplang | 2008-11-11 14:22:05 +0100 Trac importer improvements (patch #2050 by Karl Heinz Marbaise). r18772@gaspard (orig r2012): jplang | 2008-11-11 14:28:13 +0100 Fixed: Trac migration of ticket:123 or [ticket:34] do not work (#2053). r18773@gaspard (orig r2013): jplang | 2008-11-11 14:28:48 +0100 Fixed: Trac migration of ticket:123 or [ticket:34] do not work (#2053). r18774@gaspard (orig r2014): jplang | 2008-11-11 14:32:22 +0100 Fixed: Trac milestone links not correctly converted (#2052). r18775@gaspard (orig r2015): jplang | 2008-11-11 14:37:10 +0100 Documents Wiki page anchors (#1647). r18776@gaspard (orig r2016): jplang | 2008-11-11 14:49:07 +0100 Updated pt-br and zh-tw lang files. r18777@gaspard (orig r2017): jplang | 2008-11-11 14:54:10 +0100 Changes ruby bang path to #!/usr/bin/env ruby (#1876). r18778@gaspard (orig r2018): jplang | 2008-11-11 15:24:06 +0100 Turn ftps and sftp proto into links (#1514). r18779@gaspard (orig r2019): jplang | 2008-11-11 16:07:55 +0100 Adds permissions to let users edit and/or delete their messages (#854, patch by Markus Knittig with slight changes). r18780@gaspard (orig r2020): jplang | 2008-11-11 17:26:05 +0100 Less agressive Redcloth lang attribute parsing (#2091). r18781@gaspard (orig r2021): jplang | 2008-11-11 17:49:20 +0100 Hungarian language file updated. r18782@gaspard (orig r2022): jplang | 2008-11-11 19:10:21 +0100 Pluggable admin menu (patch #2031 by Yuki Sonoda with slight changes). r18783@gaspard (orig r2023): winterheart | 2008-11-12 16:13:49 +0100 update for pt-br (#2164) r18784@gaspard (orig r2024): winterheart | 2008-11-12 16:17:47 +0100 update for zh (#2151) r18785@gaspard (orig r2025): winterheart | 2008-11-12 16:18:55 +0100 Populating new strings for zh.yml r18786@gaspard (orig r2026): winterheart | 2008-11-12 16:22:57 +0100 New file for sk (#2126) r18787@gaspard (orig r2027): winterheart | 2008-11-12 16:23:52 +0100 Populating new strings for sk.yml r18788@gaspard (orig r2028): winterheart | 2008-11-12 16:34:11 +0100 update for ru r18789@gaspard (orig r2029): edavis10 | 2008-11-13 02:07:58 +0100 Changed the CSS clear on journals so they will wrap around the revisions. #2165 r18790@gaspard (orig r2030): jplang | 2008-11-13 17:39:50 +0100 Fixes #2171: issue pdf export broken by r2006. r18791@gaspard (orig r2031): jplang | 2008-11-13 17:43:39 +0100 Fixes #2170: user display format in application settings broken by r2010. r18792@gaspard (orig r2032): winterheart | 2008-11-14 16:00:23 +0100 Missed %s in label, thank Martin Bächtold for reporting (#2186) r18793@gaspard (orig r2033): winterheart | 2008-11-14 16:18:13 +0100 Translation updates (#2168, #2172, #2176, #2178) r18794@gaspard (orig r2034): winterheart | 2008-11-14 16:33:27 +0100 Polish update, #2188 r18795@gaspard (orig r2035): winterheart | 2008-11-15 09:35:17 +0100 Translation updates (#2189, #2193) r18796@gaspard (orig r2036): jplang | 2008-11-16 12:49:37 +0100 Changes version naming rule (#2162). r18797@gaspard (orig r2037): jplang | 2008-11-16 12:58:41 +0100 Moves plugin list to its own administration menu item. r18798@gaspard (orig r2038): jplang | 2008-11-16 16:22:48 +0100 Adds plugin id attribute. r18799@gaspard (orig r2039): jplang | 2008-11-16 16:38:37 +0100 Adds .find and .all Plugin class methods. r18800@gaspard (orig r2040): jplang | 2008-11-16 17:08:25 +0100 Adds a few Plugin tests. r18801@gaspard (orig r2041): jplang | 2008-11-16 18:12:02 +0100 Adds url and author_url plugin attributes (#2162). r18802@gaspard (orig r2042): jplang | 2008-11-16 21:00:20 +0100 Adds Plugin#requires_redmine method so that plugin compatibility can be checked against current Redmine version (#2162). r18803@gaspard (orig r2043): jplang | 2008-11-17 18:27:08 +0100 Do not query multiple times git for branch (#1435). r18804@gaspard (orig r2044): jplang | 2008-11-18 18:22:28 +0100 Vietnamese language updated (#2125). r18805@gaspard (orig r2045): jplang | 2008-11-18 19:36:47 +0100 SubversionAdapter#entries performance improvement. r18806@gaspard (orig r2046): jplang | 2008-11-18 22:11:25 +0100 Fixed: Printing long roadmap doesn't split across pages (#2203). r18807@gaspard (orig r2047): winterheart | 2008-11-19 16:52:09 +0100 Typo in sv, #2213 r18808@gaspard (orig r2048): jplang | 2008-11-19 20:38:19 +0100 Remove eclipse files r18811@gaspard (orig r2051): winterheart | 2008-11-21 17:35:00 +0100 fix for Polish, #2215 r18812@gaspard (orig r2052): winterheart | 2008-11-21 17:40:11 +0100 removing BOM, sorting, #2169 r18813@gaspard (orig r2053): jplang | 2008-11-22 12:44:07 +0100 Extends child_pages macro to display child pages based on page parameter (#1975). It can also be called from anywhere now (not only from wiki pages). r18814@gaspard (orig r2054): jplang | 2008-11-23 17:40:35 +0100 Fixed date filters accuracy with SQLite (#2221). r18815@gaspard (orig r2055): jplang | 2008-11-25 18:37:41 +0100 Slight tests fixes. r18816@gaspard (orig r2056): jplang | 2008-11-25 20:33:41 +0100 Do not request blank LDAP attributes. r18817@gaspard (orig r2057): winterheart | 2008-11-26 18:32:56 +0100 rake gloc:update, update for Serbian (#2232) r18819@gaspard (orig r2059): jplang | 2008-11-27 19:04:48 +0100 Adds a css class on menu items in order to apply item specific styles (eg. icons). r18820@gaspard (orig r2060): jplang | 2008-11-27 19:41:40 +0100 Typo in lang files (#2241). r18821@gaspard (orig r2061): jplang | 2008-11-27 19:43:18 +0100 Typo in gloc:update task description (#2243). r18822@gaspard (orig r2062): jplang | 2008-11-27 21:15:45 +0100 Fixed: inappropriate redirection to login or register page may occur (#2206). Eg. user clicks login link twice before logging in. r18823@gaspard (orig r2063): winterheart | 2008-11-28 16:44:59 +0100 Italian update (#2239) r18826@gaspard (orig r2066): jplang | 2008-11-30 12:18:22 +0100 Display latest user's activity on account/show view. r18827@gaspard (orig r2067): jplang | 2008-11-30 13:12:06 +0100 Makes activity view accept a user_id param to show user's activity (#1002). r18828@gaspard (orig r2068): jplang | 2008-11-30 13:14:12 +0100 Fixes activity atom link params (when not on first page). r18829@gaspard (orig r2069): jplang | 2008-11-30 13:18:59 +0100 Adds atom feed on user's account page. r18830@gaspard (orig r2070): jplang | 2008-11-30 14:38:07 +0100 Adds links between account and user's activity pages. r18831@gaspard (orig r2071): jplang | 2008-11-30 14:42:15 +0100 Slight changes to profile on account page and last connexion date added. r18832@gaspard (orig r2072): jplang | 2008-11-30 15:23:57 +0100 Obfuscates email address on user's account page using javascript. r18833@gaspard (orig r2073): jplang | 2008-11-30 15:31:01 +0100 Adds link to user's account on issue history. r18834@gaspard (orig r2074): jplang | 2008-11-30 15:55:45 +0100 Mail handler: check workflow for status set/change. r18835@gaspard (orig r2075): jplang | 2008-11-30 15:57:46 +0100 Adds status option to email integration rake tasks. r18836@gaspard (orig r2076): jplang | 2008-11-30 16:51:44 +0100 Adds --status option to rdm-mailhandler. r18837@gaspard (orig r2077): jplang | 2008-11-30 17:00:45 +0100 Adds To and Cc as watchers when submitting an issue by email (#2245). Only works if the sender has the 'Add issue watchers' permission. r18838@gaspard (orig r2078): jplang | 2008-11-30 17:34:39 +0100 Changes Portuguese decimal separator (#1372). r18839@gaspard (orig r2079): jplang | 2008-11-30 17:57:56 +0100 Replaces User.find_active with a named scope. r18840@gaspard (orig r2080): winterheart | 2008-12-01 17:00:54 +0100 Translation updates (#2249, #2250, #2252, #2254) r18841@gaspard (orig r2081): winterheart | 2008-12-01 17:11:05 +0100 ru.yml update r18842@gaspard (orig r2082): jplang | 2008-12-01 18:27:44 +0100 Fixed: 404 when "Apply" clicked on activity page (#2251). r18843@gaspard (orig r2083): jplang | 2008-12-02 18:16:06 +0100 Fixed: activity broken by r2066 with postgresql (#2266). r18844@gaspard (orig r2084): jplang | 2008-12-02 18:29:52 +0100 Use style attribute for setting width of table cells in progress bars (#2267). r18845@gaspard (orig r2085): jplang | 2008-12-02 18:57:13 +0100 Fixed: wrong digest for text files under Windows (#2264). r18846@gaspard (orig r2086): edavis10 | 2008-12-04 00:18:07 +0100 Added :controller_issues_edit_before_save hook r18847@gaspard (orig r2087): edavis10 | 2008-12-04 00:18:12 +0100 Added :view_issues_edit_notes_bottom hook r18848@gaspard (orig r2088): jplang | 2008-12-05 16:41:32 +0100 Cross-project gantt and calendar (#1157). r18849@gaspard (orig r2089): edavis10 | 2008-12-05 22:03:55 +0100 Added :view_issues_history_journal_bottom hook r18850@gaspard (orig r2090): edavis10 | 2008-12-05 23:56:03 +0100 Refactor: Extracted new method Query#sql_for_field from Query#statement in order to clean up Query#statement. r18851@gaspard (orig r2091): edavis10 | 2008-12-05 23:56:08 +0100 Bit more refactoring on Query#sql_for_field to remove multiple returns r18852@gaspard (orig r2092): edavis10 | 2008-12-05 23:56:13 +0100 Final refactoring on Query#sql_for_field to rename v to value r18853@gaspard (orig r2093): edavis10 | 2008-12-06 01:51:03 +0100 Added several useful hooks to the Issue sidebar * :view_issues_sidebar_issues_bottom * :view_issues_sidebar_planning_bottom * :view_issues_sidebar_queries_bottom r18854@gaspard (orig r2094): jplang | 2008-12-06 12:21:10 +0100 Changes issue history headings. r18855@gaspard (orig r2095): jplang | 2008-12-06 18:20:37 +0100 Fixes Darcs#cat with Postgresql. r18856@gaspard (orig r2096): jplang | 2008-12-06 18:40:54 +0100 Fixed: CVS connexion string may not contain @. r18857@gaspard (orig r2097): jplang | 2008-12-06 19:01:20 +0100 Slight change to css so that gravatar is vertically centered on user's page. r18858@gaspard (orig r2098): jplang | 2008-12-06 23:40:50 +0100 Translations updates. r18860@gaspard (orig r2100): jplang | 2008-12-07 09:41:54 +0100 Changelog updated. r18861@gaspard (orig r2101): jplang | 2008-12-07 09:48:29 +0100 Show project name in front of related issues if cross-project issue relations are enabled (#2282). r18862@gaspard (orig r2102): jplang | 2008-12-07 10:53:27 +0100 Upgrade to Rails 2.1.2 r18863@gaspard (orig r2103): jplang | 2008-12-07 10:54:37 +0100 Set version to 0.8 r18864@gaspard (orig r2104): jplang | 2008-12-07 10:56:28 +0100 Update changelog for 0.8 rc1 r18865@gaspard (orig r2105): jplang | 2008-12-07 10:59:19 +0100 UPGRADING updated r18869@gaspard (orig r2109): jplang | 2008-12-07 14:12:19 +0100 Makes logged-in username in topbar linking to (#2291). r18870@gaspard (orig r2110): jplang | 2008-12-07 15:40:33 +0100 Use options hash in UnifiedDiff.new r18871@gaspard (orig r2111): jplang | 2008-12-07 15:44:08 +0100 Follows r2110. r18872@gaspard (orig r2112): jplang | 2008-12-07 16:21:40 +0100 Adds a setting to limit the number of diff lines that should be displayed (default to 1500). r18874@gaspard (orig r2114): jplang | 2008-12-08 19:20:26 +0100 Fixed: project activity truncated after viewing user's activity. r18876@gaspard (orig r2116): jplang | 2008-12-09 17:54:46 +0100 AttachmentsController now handles attachments deletion. r18877@gaspard (orig r2117): jplang | 2008-12-09 19:00:27 +0100 Files module: makes version field non required (#1053). r18878@gaspard (orig r2118): jplang | 2008-12-09 19:30:22 +0100 Fixed: Firefox cuts off large diffs (#2234). r18879@gaspard (orig r2119): winterheart | 2008-12-10 18:01:39 +0100 Translation updates (#2310, #2309, #2306, #2304, #2302, #2300, #2299) r18880@gaspard (orig r2120): winterheart | 2008-12-10 18:13:04 +0100 russian update r18881@gaspard (orig r2121): edavis10 | 2008-12-11 00:44:22 +0100 Added plugin hooks around Journal editing * :controller_journals_edit_post * :view_journals_notes_form_after_notes * :view_journals_update_rjs_bottom r18882@gaspard (orig r2122): jplang | 2008-12-12 13:07:09 +0100 Makes User.find_by_mail case-insensitive (password reminder #2322, repo users mapping). r18883@gaspard (orig r2123): jplang | 2008-12-12 14:32:39 +0100 Fixed: default flag removed when editing a default enumeration (#2327). r18884@gaspard (orig r2124): jplang | 2008-12-12 14:49:14 +0100 Fixed: default category ignored when adding a document (#2328). r18885@gaspard (orig r2125): jplang | 2008-12-12 17:01:35 +0100 Escape back_url field value (#2320). r18886@gaspard (orig r2126): jplang | 2008-12-12 17:03:57 +0100 Rescue back_url param parsing on redirect. r18887@gaspard (orig r2127): jplang | 2008-12-12 17:04:54 +0100 Undo unwanted change. r18888@gaspard (orig r2128): jplang | 2008-12-12 17:07:14 +0100 Capture scm CLI stderr to log/scm.stderr.log when running in dev environment r18889@gaspard (orig r2129): jplang | 2008-12-12 20:11:16 +0100 Make use of User.find_by_mail r18890@gaspard (orig r2130): winterheart | 2008-12-12 20:34:31 +0100 translation updates r18891@gaspard (orig r2131): winterheart | 2008-12-12 20:41:12 +0100 Fixing quotes r18894@gaspard (orig r2134): jplang | 2008-12-14 16:36:59 +0100 Rails 2.1.2 deprecations (#2332). r18895@gaspard (orig r2135): jplang | 2008-12-14 16:57:13 +0100 Fixed: CVS browser should not show dead revisions (deleted files) (#2319). r18896@gaspard (orig r2136): jplang | 2008-12-14 18:10:16 +0100 Mail handler: strip tags when receiving a html-only email (#2312). r18897@gaspard (orig r2137): jplang | 2008-12-15 19:02:25 +0100 Fixes repository user mapping submission when a repository username is blank (#2339, Conflicting types for parameter containers). r18899@gaspard (orig r2139): jplang | 2008-12-16 22:11:37 +0100 Adds a helper that returns issues css classes. r18900@gaspard (orig r2140): jplang | 2008-12-16 22:13:35 +0100 Adds a css class (overdue) to overdue issues on issue lists and detail views (#2337). r18901@gaspard (orig r2141): edavis10 | 2008-12-18 08:10:23 +0100 Fixed a failing test caused by comparing a Time object (n.day.ago) with a Date object r18902@gaspard (orig r2142): winterheart | 2008-12-18 23:27:32 +0100 Typo on translation, #2352 r18903@gaspard (orig r2143): jplang | 2008-12-19 09:10:35 +0100 Escape textarea content when editing a issue note. r18904@gaspard (orig r2144): jplang | 2008-12-19 11:16:15 +0100 Escape double-quotes in image titles. r18905@gaspard (orig r2145): jplang | 2008-12-19 11:43:06 +0100 Check that wiki page exists before processing (#2360). r18907@gaspard (orig r2147): jplang | 2008-12-19 15:13:24 +0100 CHANGELOG updated. r18924@gaspard (orig r2164): jplang | 2008-12-22 20:21:02 +0100 Adds watchers selection on new issue form (#398). Permission 'add issue watchers' required. r18925@gaspard (orig r2165): jplang | 2008-12-22 20:24:17 +0100 Do not hardcode Watcher string in r2164. r18926@gaspard (orig r2166): jplang | 2008-12-22 20:25:07 +0100 Sligth change to fr.yml. r18927@gaspard (orig r2167): jplang | 2008-12-22 21:33:01 +0100 Show view/annotate/download links on repositories/entries and repositories/annotate views (#2367). r18928@gaspard (orig r2168): jplang | 2008-12-23 01:16:26 +0100 Escape wiki annotate lines content (#2380). r18929@gaspard (orig r2169): jplang | 2008-12-23 01:19:15 +0100 Escape query names (#2379). r18930@gaspard (orig r2170): jplang | 2008-12-23 18:05:38 +0100 Escape textile titles and styles (#2377). r18931@gaspard (orig r2171): jplang | 2008-12-24 11:03:13 +0100 Validates sort_key and sort_order params (#2378). r18938@gaspard (orig r2178): jplang | 2008-12-24 14:29:43 +0100 Fixes a JS error on context_menu with IE (#2390). r18940@gaspard (orig r2180): winterheart | 2008-12-24 16:44:43 +0100 #2329, swedish lang update r18941@gaspard (orig r2181): winterheart | 2008-12-24 16:47:24 +0100 #2368, pt.yml update r18942@gaspard (orig r2182): winterheart | 2008-12-24 16:48:59 +0100 #2386, korean translation update r18943@gaspard (orig r2183): jplang | 2008-12-27 15:05:03 +0100 Prevent SQL error with old sessions after r2171. r18946@gaspard (orig r2186): jplang | 2008-12-27 18:49:01 +0100 Fixtures update. r18947@gaspard (orig r2187): jplang | 2008-12-27 19:07:46 +0100 Fixes functional test failures. r18948@gaspard (orig r2188): jplang | 2008-12-27 19:10:36 +0100 Do not show a link to the current annotate or view page (#2367). r18949@gaspard (orig r2189): jplang | 2008-12-27 19:33:35 +0100 Fixed: deleted files should not be shown when browsing a Darcs repository (#2385). r18950@gaspard (orig r2190): jplang | 2008-12-28 10:46:16 +0100 Fixes functional tests fixtures (#2398). r18951@gaspard (orig r2191): jplang | 2008-12-28 11:12:09 +0100 Fixed bold syntax around single character in series (#2351). r18952@gaspard (orig r2192): jplang | 2008-12-28 14:38:34 +0100 Disable textile inline styles to prevent XSS attacks (#2377). r18955@gaspard (orig r2195): jplang | 2008-12-28 15:48:23 +0100 Mail handler: add watchers before sending notification (#2245). r18956@gaspard (orig r2196): jplang | 2008-12-29 13:40:56 +0100 Renumbers projects_trackers fixtures (#2411). r18959@gaspard (orig r2199): jplang | 2008-12-29 16:43:42 +0100 Translations updates. r18961@gaspard (orig r2201): jplang | 2008-12-29 17:08:31 +0100 CHANGELOG updated. r18962@gaspard (orig r2202): winterheart | 2008-12-29 19:27:27 +0100 #2373, fixing encoding r18968@gaspard (orig r2208): jplang | 2008-12-30 14:32:14 +0100 CHANGELOG updated. r18969@gaspard (orig r2209): jplang | 2008-12-30 14:32:51 +0100 Increment project files downloads. r18970@gaspard (orig r2210): jplang | 2008-12-30 15:24:51 +0100 Jump to the current tab when using the project quick-jump combo (#2364). r18971@gaspard (orig r2211): jplang | 2008-12-30 15:57:33 +0100 Import custom fields values from emails (#2413). r18972@gaspard (orig r2212): jplang | 2008-12-30 17:23:05 +0100 Stricter textile links parsing (#2417). r18973@gaspard (orig r2213): jplang | 2008-12-30 17:43:26 +0100 Changes pt-br decimal separator (#1372). r18974@gaspard (orig r2214): jplang | 2008-12-31 11:39:33 +0100 Do not escape back_url twice when login fails. r18978@gaspard (orig r2218): jplang | 2008-12-31 12:48:56 +0100 Admin Info Screen: Display if plugin assets directory is writable (#2425). r18979@gaspard (orig r2219): jplang | 2008-12-31 14:59:30 +0100 Fix sv lang file r18980@gaspard (orig r2220): jplang | 2008-12-31 15:56:30 +0100 IMAP: add options to move received emails. r18981@gaspard (orig r2221): jplang | 2009-01-03 14:09:36 +0100 Lower the project identifier limit to a minimum of two characters (#2003). r18982@gaspard (orig r2222): jplang | 2009-01-03 14:14:28 +0100 Fixed: syntax highlight doesn't appear in new ticket preview (#1976). r18983@gaspard (orig r2223): jplang | 2009-01-03 15:11:44 +0100 Moves flash messages rendering to a helper method. r18984@gaspard (orig r2224): jplang | 2009-01-03 15:44:12 +0100 Display a warning if some attachments were not saved (#2008). r18985@gaspard (orig r2225): jplang | 2009-01-03 17:03:12 +0100 Fixed: email notification for changes I make still occurs when running Repository.fetch_changesets (#1957). r18986@gaspard (orig r2226): jplang | 2009-01-04 13:03:39 +0100 Move PDF stuff to a single helper. r18987@gaspard (orig r2227): jplang | 2009-01-04 13:14:05 +0100 Makes the app boot with Rails 2.2.2 r18988@gaspard (orig r2228): jplang | 2009-01-04 13:50:45 +0100 Do not use compute_public_path. r18992@gaspard (orig r2232): jplang | 2009-01-04 14:27:48 +0100 Merged r2231 from 0.8-stable (#2402). r18993@gaspard (orig r2233): jplang | 2009-01-04 15:54:19 +0100 Scramble PDF title (#1204). r18994@gaspard (orig r2234): jplang | 2009-01-04 18:09:25 +0100 Slight changes to ease Rails 2.2 support. r18995@gaspard (orig r2235): jplang | 2009-01-04 19:14:51 +0100 Slight changes in functional tests. r19006@gaspard (orig r2246): jplang | 2009-01-07 20:47:24 +0100 Makes issue description a non-required field (#2456). r19007@gaspard (orig r2247): jplang | 2009-01-07 21:03:33 +0100 Refactor TabularFormBuilder field helpers (#2461). r19008@gaspard (orig r2248): jplang | 2009-01-07 21:21:27 +0100 Fixes functional test broken by r2246. r19009@gaspard (orig r2249): jplang | 2009-01-07 21:22:06 +0100 Fixes a test failure with svn < 1.5 (#2455). r19010@gaspard (orig r2250): jplang | 2009-01-07 21:30:02 +0100 Adds 'closed' css class to closed issues in the issue list (#2458). r19011@gaspard (orig r2251): jplang | 2009-01-09 18:32:46 +0100 Fixed: no error is raised when entering invalid hours on the issue update form (#2465). r19013@gaspard (orig r2253): jplang | 2009-01-10 12:29:35 +0100 Makes email adress uniqueness case-insensitive (#2473). r19016@gaspard (orig r2256): jplang | 2009-01-11 12:01:35 +0100 Different icon for closed issues in search result (#992). r19017@gaspard (orig r2257): jplang | 2009-01-11 17:33:51 +0100 Ability to sort the issue list by text, list, date and boolean custom fields (#1139). r19018@gaspard (orig r2258): jplang | 2009-01-11 19:38:07 +0100 Ability to sort the issue list by text, int and float custom fields (#1139). r19019@gaspard (orig r2259): jplang | 2009-01-11 20:48:16 +0100 Use margin-right instead of padding-right on top menu links. r19020@gaspard (orig r2260): edavis10 | 2009-01-12 05:44:01 +0100 Codified instructions from RUNNING_TESTS as rake tasks for convenience Rake tasks are in testing.rake and can be run by `rake test:scm:setup:<scm>` Updated RUNNING_TESTS Contributed by Gerrit Kaiser r19021@gaspard (orig r2261): edavis10 | 2009-01-12 05:52:56 +0100 Added two new plugin hooks to IssuesController: * :controller_issues_new_after_save * :controller_issues_edit_after_save #2475 r19022@gaspard (orig r2262): jplang | 2009-01-12 18:45:23 +0100 Fixes r2226: exporting an issue with attachments to PDF raises an error (#2492). r19023@gaspard (orig r2263): jplang | 2009-01-12 18:46:53 +0100 Typo (#2489). r19025@gaspard (orig r2265): jplang | 2009-01-16 18:20:41 +0100 Adds a 'Create and continue' button on the new issue form, that will create the issue and display the form again (#2523). r19026@gaspard (orig r2266): jplang | 2009-01-16 21:57:18 +0100 Makes subject field get focus on 'New issue' form (#2522). r19027@gaspard (orig r2267): jplang | 2009-01-16 22:02:03 +0100 Use a textarea for custom fields possible values (#2472). r19028@gaspard (orig r2268): jplang | 2009-01-16 22:02:56 +0100 Adds custom fields functional tests. r19029@gaspard (orig r2269): jplang | 2009-01-17 08:53:32 +0100 Slight visual changes on the issue form. r19030@gaspard (orig r2270): jplang | 2009-01-17 09:03:53 +0100 Do not show Category field when categories are not defined. r19031@gaspard (orig r2271): jplang | 2009-01-17 09:08:33 +0100 Project jump box fix. r19032@gaspard (orig r2272): jplang | 2009-01-17 09:25:55 +0100 Make use of tracker_ids association in issue custom field form. r19033@gaspard (orig r2273): jplang | 2009-01-17 09:41:30 +0100 CustomFieldsController refactoring. r19034@gaspard (orig r2274): jplang | 2009-01-17 09:46:23 +0100 CustomFieldsController#list moved to #index. r19035@gaspard (orig r2275): jplang | 2009-01-17 10:04:10 +0100 Moves a few settings to a "Display" panel. r19036@gaspard (orig r2276): jplang | 2009-01-17 12:18:04 +0100 User custom fields can now be set as editable so that users can edit them on 'My account'. For existing user custom fields, this new attribute is set to false by default to preserve the prior behaviour (it can turned on by editing the custom field in admin area). Note: on the registration form, *required* custom fields will be displayed even if they are not defined as editable so that the account can be created. r19039@gaspard (orig r2279): jplang | 2009-01-18 11:54:08 +0100 Fixes 103_set_custom_fields_editable migration from r2276 (#2526). r19040@gaspard (orig r2280): jplang | 2009-01-18 12:54:56 +0100 Fixed that Trac importer was creating duplicate custom values (#2506). r19041@gaspard (orig r2281): jplang | 2009-01-18 16:16:31 +0100 Adds Message-Id and References headers to email notifications so that issues and messages threads can be displayed by email clients (#1401). r19042@gaspard (orig r2282): jplang | 2009-01-18 21:00:03 +0100 Fix in AttachmentsController#show. r19043@gaspard (orig r2283): winterheart | 2009-01-19 16:55:54 +0100 #2439, translation update r19044@gaspard (orig r2284): winterheart | 2009-01-19 16:57:19 +0100 #2442, translation update r19045@gaspard (orig r2285): winterheart | 2009-01-19 17:02:57 +0100 #2429, translation update r19046@gaspard (orig r2286): winterheart | 2009-01-19 17:06:39 +0100 #2442, small fix r19047@gaspard (orig r2287): winterheart | 2009-01-19 17:43:28 +0100 translation updates (#2535, #2505, #2524, #2434) r19048@gaspard (orig r2288): jplang | 2009-01-19 19:29:07 +0100 Use In-Reply-To and References headers to handle replies by email. r19049@gaspard (orig r2289): jplang | 2009-01-19 20:03:53 +0100 Allow email to reply to a forum message (#1616). r19050@gaspard (orig r2290): winterheart | 2009-01-20 16:45:34 +0100 #2453, sv.yml patch, some errors still exist (see ticket) r19051@gaspard (orig r2291): winterheart | 2009-01-20 16:53:09 +0100 #2445, nl.yml update r19052@gaspard (orig r2292): winterheart | 2009-01-20 17:09:07 +0100 #2463, partially solved r19053@gaspard (orig r2293): winterheart | 2009-01-20 17:13:14 +0100 #2540, pt-br update r19054@gaspard (orig r2294): jplang | 2009-01-21 19:22:30 +0100 Accept replies to forum messages by subject recognition (#1616). r19055@gaspard (orig r2295): jplang | 2009-01-22 17:34:54 +0100 Automatically focus several form fields. r19056@gaspard (orig r2296): winterheart | 2009-01-23 16:37:59 +0100 New Galician Translation (#2547), thanks to Martín Vázquez for intial translation r19057@gaspard (orig r2297): winterheart | 2009-01-23 16:40:38 +0100 #2562, update for zh.yml r19058@gaspard (orig r2298): winterheart | 2009-01-23 16:46:22 +0100 Translation updates (#2453, #2463, #2551) r19059@gaspard (orig r2299): winterheart | 2009-01-23 16:58:58 +0100 removing \r\n r19060@gaspard (orig r2300): winterheart | 2009-01-23 17:30:04 +0100 ru.yml update r19062@gaspard (orig r2302): jplang | 2009-01-24 09:58:03 +0100 Fixed: Details time log report CSV export doesn't honour date format from settings (patch #2466 by Russell Hind). r19063@gaspard (orig r2303): jplang | 2009-01-24 10:02:55 +0100 Fixes a test that was broken by r2294. r19064@gaspard (orig r2304): jplang | 2009-01-24 12:31:15 +0100 Merged nested projects branch. Removes limit on subproject nesting (#594). r19065@gaspard (orig r2305): jplang | 2009-01-24 12:48:38 +0100 Removes unused projects_count column from projects table. r19071@gaspard (orig r2311): jplang | 2009-01-25 12:15:28 +0100 Ignore archived subprojects in Project#rolled_up_trackers (#2550). r19072@gaspard (orig r2312): jplang | 2009-01-25 13:13:27 +0100 Fixed that the project jump box does not preserve current tab after r2304. r19073@gaspard (orig r2313): jplang | 2009-01-25 14:12:56 +0100 Adds ability to bulk copy issues (#1847). This can be done by checking the 'Copy' checkbox on the 'Move' form. r19074@gaspard (orig r2314): jplang | 2009-01-25 14:18:44 +0100 Removes spaces before colons. r19075@gaspard (orig r2315): jplang | 2009-01-25 14:52:40 +0100 Render the project list as a tree on Move form. r19076@gaspard (orig r2316): jplang | 2009-01-25 17:04:28 +0100 Ability to bulk edit custom fields of type 'list' (#461). r19077@gaspard (orig r2317): edavis10 | 2009-01-26 02:47:51 +0100 Converted routing and urls to follow the Rails REST convention. Patch supplied by commits from Gerrit Kaiser on Github. Existing routes will still work (backwards compatible) but any new urls will be generated using the new routing rules. Changes listed below: * made the URLs for some project tabs and project settings follow the new rails RESTful conventions of /collection/:id/subcollection/:sub_id * prettier URL for project roadmap * more nice project URLs * use GET for filtering form * prettified URLs used on issues tab * custom route for activity atom feeds * prettier repository urls * fixed broken route definition * fixed failing tests for issuecontroller that were hardcoding the url string * more RESTful routes for boards and messages * RESTful routes for wiki pages * RESTful routes for documents * moved old routes that are retained for compatibility to the bottom and grouped them together * added RESTful URIs for issues * RESTfulness for the news section * fixed route order * changed hardcoded URLs in tests * fixed badly written tests * fixed forgotten parameter in routes * changed hardcoded URLS to new scheme * changed project add url to the standard POST to collection * create new issue by POSTing to collection * changed hardcoded URLs in integrations tests * made project add form work again * restful routes for project deletion * prettier routes for project (un)archival * made routes table more readable * fixed note quoting * user routing * fixed bug * always sort by GET * Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled. * prettified URLs used on issues tab * urls for time log * fixed reply routing * eliminate revision query paremeter for diff and entry actions * fixed test failures with hard-coded urls * ensure ajax links always use get * refactored ajax link generation into separate method #1901 r19078@gaspard (orig r2318): jplang | 2009-01-26 18:43:58 +0100 Fixes activity pagination broken by r2317. r19079@gaspard (orig r2319): jplang | 2009-01-27 18:27:50 +0100 Replaces the obsolete robots.txt with a cached action (#2491). r19080@gaspard (orig r2320): jplang | 2009-01-27 18:40:55 +0100 Fixed actions on issues (gantt, calendar, move, bulk_edit...) at global level broken by r2317. r19081@gaspard (orig r2321): jplang | 2009-01-27 18:58:56 +0100 Explicitly require 'rfpdf/fpdf' (#2584). r19082@gaspard (orig r2322): jplang | 2009-01-27 19:19:27 +0100 Fixed that 'My page' blocks may display issues that the user is no longer allowed to view (#2590). r19083@gaspard (orig r2323): jplang | 2009-01-27 20:33:03 +0100 Fixed: users should not be able to add relations with issues they're not allowed to view (#2589). r19084@gaspard (orig r2324): edavis10 | 2009-01-27 21:42:19 +0100 Fixes Issue sorting in a project, broken by #2317 Issues were sorting but the project id wasn't being added so the IssuesController would return all issues (cross-project). r19085@gaspard (orig r2325): edavis10 | 2009-01-27 21:59:02 +0100 Fixed clearing the Issue filters in the issue list, broken by #2317 r19086@gaspard (orig r2326): jplang | 2009-01-28 21:52:39 +0100 Fixed user's activity atom feed broken by r2317. r19087@gaspard (orig r2327): jplang | 2009-01-28 22:11:13 +0100 Fixed calendar navigation links broken by r2317. r19088@gaspard (orig r2328): jplang | 2009-01-28 22:20:39 +0100 Fixing calendar and gantt links broken by r2317. r19089@gaspard (orig r2329): jplang | 2009-01-28 22:25:35 +0100 Fixed project news atom link broken by r2317. r19090@gaspard (orig r2330): jplang | 2009-01-29 10:05:36 +0100 Sort target versions list on bulk edit form (#2616). r19091@gaspard (orig r2331): jplang | 2009-01-29 12:09:46 +0100 Fixes other formats download links on the project issue list (project_id lost) broken r2317. r19092@gaspard (orig r2332): jplang | 2009-01-29 13:26:32 +0100 Fixed an error when downloading gantt png at global level. r19093@gaspard (orig r2333): jplang | 2009-01-29 14:53:17 +0100 Adds an helper to render other formats download links. r19094@gaspard (orig r2334): jplang | 2009-01-29 14:54:44 +0100 Adds rel='nofollow' attribute to other formats download links (#2491). r19095@gaspard (orig r2335): jplang | 2009-01-29 15:22:56 +0100 Adds projects association on tracker form (#2578). r19096@gaspard (orig r2336): jplang | 2009-01-29 17:33:45 +0100 Fixed: TOC does not parse wiki page reference links with description (#2601). r19097@gaspard (orig r2337): jplang | 2009-01-29 17:34:00 +0100 Cleaning test. r19098@gaspard (orig r2338): jplang | 2009-01-30 18:50:28 +0100 Changes time related icons. r19099@gaspard (orig r2339): jplang | 2009-01-31 12:43:54 +0100 Adds :async_smtp and :async_sendmail delivery methods to perform email deliveries asynchronously. Code from http://www.datanoise.com/articles/2006/7/14/asynchronous-email-delivery. r19100@gaspard (orig r2340): winterheart | 2009-01-31 13:02:37 +0100 New translation - Slovenian, thank to Nejc Vidmar for work (#2577), translation updates (#2129, #2586) r19101@gaspard (orig r2341): jplang | 2009-01-31 13:42:02 +0100 Updates footer year. r19102@gaspard (orig r2342): jplang | 2009-01-31 13:48:09 +0100 Removes Issue.visible_by r19103@gaspard (orig r2343): jplang | 2009-01-31 14:22:29 +0100 Fixed: issue details view discloses relations to issues that the user is not allowed to view (#2589). r19104@gaspard (orig r2344): jplang | 2009-01-31 15:50:56 +0100 Less strict textile links parsing (#2582). r19105@gaspard (orig r2345): jplang | 2009-02-01 15:36:38 +0100 Fixed: Contextual divs after attachments are placed incorrectly in FireFox (#2633). r19106@gaspard (orig r2346): jplang | 2009-02-01 16:48:56 +0100 Do not repeat one-line commit logs on the activity view. r19107@gaspard (orig r2347): jplang | 2009-02-01 16:57:01 +0100 Show line breaks in activity events summary. r19108@gaspard (orig r2348): jplang | 2009-02-01 17:00:20 +0100 Changes color of activity events/search results summary. r19109@gaspard (orig r2349): jplang | 2009-02-01 19:54:05 +0100 Use estimated hours to weight issues in version completion calculation (#2182). r19110@gaspard (orig r2350): jplang | 2009-02-01 20:54:50 +0100 Adds a setting to limit the number of revisions displayed on a repository file log (default=100). r19111@gaspard (orig r2351): jplang | 2009-02-01 21:56:10 +0100 Include both last and first name when sorting issues by assignee (#1841). r19112@gaspard (orig r2352): jplang | 2009-02-01 21:57:44 +0100 Include both version date and name when sorting issues by target version (#1502). r19113@gaspard (orig r2353): jplang | 2009-02-02 18:34:12 +0100 Adds a 'box' div around news comment form (#2632). r19119@gaspard (orig r2359): jplang | 2009-02-03 18:13:37 +0100 Fixes message search eager loading (#2654). r19120@gaspard (orig r2360): jplang | 2009-02-03 18:15:59 +0100 Typos/fixes in views (#2654). r19121@gaspard (orig r2361): jplang | 2009-02-03 18:32:07 +0100 Closed issue are not overdue, fixes r2140 (#2337). r19122@gaspard (orig r2362): jplang | 2009-02-05 18:43:49 +0100 Typo in wiki link example (#2673). r19123@gaspard (orig r2363): jplang | 2009-02-05 21:25:01 +0100 Fixed: inline attached image should not match partial filename (#2683). r19159@gaspard (orig r2399): jplang | 2009-02-07 21:11:03 +0100 Fixed: path parameter is not an array when changing diff style (#2695), broken by r2317. r19175@gaspard (orig r2415): jplang | 2009-02-08 18:24:39 +0100 Fixed: migration 98 breaks when using table name prefix. r19183@gaspard (orig r2423): jplang | 2009-02-09 18:18:41 +0100 Fixed: TypeError (can't modify frozen string) on settings view (#2700). r19184@gaspard (orig r2424): jplang | 2009-02-09 18:24:06 +0100 Removes hardcoded table names (#2701). r19186@gaspard (orig r2426): jplang | 2009-02-09 21:17:58 +0100 Strip keywords from received email body (#2436). r19187@gaspard (orig r2427): edavis10 | 2009-02-10 02:18:49 +0100 Added plugin hook :view_projects_roadmap_version_bottom. #2543 r19188@gaspard (orig r2428): edavis10 | 2009-02-10 02:24:32 +0100 Added two new plugin hooks: * :view_layouts_base_sidebar * :view_layouts_base_content r19189@gaspard (orig r2429): edavis10 | 2009-02-10 04:12:40 +0100 Added request and controller objects to the hooks by default. The request and controller objects are now added to all hook contexts by default. This will also make url_for work better in hooks by setting up the default_url_options :host, :port, and :protocol. Finally a new helper method @render_or@ has been added to ViewListener. This will let a hook easily render a partial without a full method definition. Thanks to Thomas Löber for the original patch. #2542 r19190@gaspard (orig r2430): edavis10 | 2009-02-10 04:12:45 +0100 Renamed variables to be more descriptive. #2542 r19191@gaspard (orig r2431): winterheart | 2009-02-10 16:41:05 +0100 Updated translations (#2577, #2640, #2644, #2652) r19192@gaspard (orig r2432): winterheart | 2009-02-10 16:57:52 +0100 Translation updates (#2643, #2645, #2668) r19193@gaspard (orig r2433): winterheart | 2009-02-10 17:05:31 +0100 New language - Macedonian (mk). Thank to Ilin Tatabitovski for work. r19194@gaspard (orig r2434): jplang | 2009-02-10 18:18:19 +0100 Fixes broken action url on time edit form (#2707). r19195@gaspard (orig r2435): jplang | 2009-02-10 23:03:25 +0100 Replaces the repositories management SOAP API with a simple REST API. reposman usage is unchanged but the script now requires activeresource. actionwebservice is now longer used and thus removed from plugins. r19196@gaspard (orig r2436): jplang | 2009-02-10 23:54:22 +0100 Leave wiki links untouched if target project doesn't exist or have no wiki. r19197@gaspard (orig r2437): edavis10 | 2009-02-11 20:06:37 +0100 Unpacked OpenID gem. #699 r19198@gaspard (orig r2438): edavis10 | 2009-02-11 20:06:45 +0100 Added open_id_authentication plugin r19199@gaspard (orig r2439): edavis10 | 2009-02-11 20:06:50 +0100 Added OpenID tables. #699 r19200@gaspard (orig r2440): edavis10 | 2009-02-11 20:06:55 +0100 Added identity_url to User. #699 r19201@gaspard (orig r2441): edavis10 | 2009-02-11 20:07:00 +0100 Fixed a bug in open_id_authentication, where relative_url_root is defined on ActionController:AbstractRequest not Base #699 r19202@gaspard (orig r2442): edavis10 | 2009-02-11 20:07:07 +0100 Added the ability to login via OpenID. * Refactored AccountController#login to use either password or openid based authentication * Extracted AccountController#successful_authentication to setup a user's session cookies and redirect * Implemented the start of AccountController#open_id_authentication which will check with the OpenID server and perform authentication. * Added text field for the OpenID url to /login * Added identity_url for OpenID to the user forms. * Added option to login with OpenID to the register form. * Added a root url route, which is used by the OpenID plugin #699 r19203@gaspard (orig r2443): edavis10 | 2009-02-11 20:07:12 +0100 Hooked up on the fly OpenID user creation. * Use OpenID registration fields for the user. * Generate a random password when a user is created. r19204@gaspard (orig r2444): edavis10 | 2009-02-11 20:07:18 +0100 Adding OpenID mock and test. #699 r19205@gaspard (orig r2445): edavis10 | 2009-02-11 20:07:23 +0100 Added tests for the other OpenID authentication cases. #699 r19206@gaspard (orig r2446): edavis10 | 2009-02-11 20:07:28 +0100 Added user setup needed based on the system's registration settings * Copied the register action's chunk of code used to setup the account based on Setting.self_registration * Extracted method for when onthefly_creation_failed * Added tests to confirm the behavior #699 r19207@gaspard (orig r2447): edavis10 | 2009-02-11 20:07:34 +0100 Refactored common methods out of register and open_id_authenticate * Extracted register_by_email_activation * Extracted register_automatically * Extracted register_manually_by_administrator #699 r19208@gaspard (orig r2448): edavis10 | 2009-02-11 20:07:41 +0100 Prevent registration via OpenID if self registration is off. #699 r19209@gaspard (orig r2449): edavis10 | 2009-02-11 20:24:28 +0100 Added a system setting for allowing OpenID logins and registrations * Defaults to off * Is set in the Administration panel under Authentication #699 r19210@gaspard (orig r2450): edavis10 | 2009-02-11 20:45:53 +0100 Added a space so words don't runtogeatherlikethis. #699 r19211@gaspard (orig r2451): jplang | 2009-02-11 21:25:05 +0100 Slight changes to the issue lists displayed on My page. r19212@gaspard (orig r2452): edavis10 | 2009-02-12 02:32:50 +0100 Fixed the bundled ruby-openid gem * The open_id_authentication plugin will require the gem automatically so it doesn't need to be added to environment.rb * Changed the version requirement on the open_id_authentication to match the latest stable version. Rails config.gem looks for a directory named after that specific version and will not load newer versions. #699 r19213@gaspard (orig r2453): edavis10 | 2009-02-12 05:31:28 +0100 Normalize the identity_url when it's set. OpenId uses a specific format for the url it uses which requires the protocol and trailing slash. This change will normalize the value to when a user sets it. #699 r19214@gaspard (orig r2454): jplang | 2009-02-12 18:19:32 +0100 Hide openid stuff on my account if disabled (#699). r19215@gaspard (orig r2455): jplang | 2009-02-12 18:30:56 +0100 Adds missing strings (#699). r19216@gaspard (orig r2456): jplang | 2009-02-12 18:35:57 +0100 Adds ability to filter watched issues (#846). r19217@gaspard (orig r2457): jplang | 2009-02-12 18:38:36 +0100 Link to watched issues list on my page. r19218@gaspard (orig r2458): jplang | 2009-02-12 22:25:50 +0100 Removes the fat ruby-openid gem. Simply use 'gem install ruby-openid' to enable openid support. r19219@gaspard (orig r2459): jplang | 2009-02-12 23:01:20 +0100 Issues pagination loses project param after applying or clearing filter (#2726). r19220@gaspard (orig r2460): jplang | 2009-02-12 23:14:22 +0100 Adds watch/unwatch link on the issue context menu (#2730). r19221@gaspard (orig r2461): jplang | 2009-02-13 18:29:49 +0100 Removes invalid css class on issue details (#2733). r19223@gaspard (orig r2463): jplang | 2009-02-13 18:59:45 +0100 Timelog is ignored when updating an issue if user is admin but not a project member (#2717). git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/nbc@2464 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
1 parent
7e46d9b35b
commit
8c669eeb80
491 files changed
+21745
-12563
No files matched your search
-265
@@ -1,265 +0,0 @@
|
||||
*SVN*
|
||||
|
||||
* Documentation for ActionWebService::API::Base. Closes #7275. [zackchandler]
|
||||
|
||||
* Allow action_web_service to handle various HTTP methods including GET. Closes #7011. [zackchandler]
|
||||
|
||||
* Ensure that DispatcherError is being thrown when a malformed request is received. [Kent Sibilev]
|
||||
|
||||
* Added support for decimal types. Closes #6676. [Kent Sibilev]
|
||||
|
||||
* Removed deprecated end_form_tag helper. [Kent Sibilev]
|
||||
|
||||
* Removed deprecated @request and @response usages. [Kent Sibilev]
|
||||
|
||||
* Removed invocation of deprecated before_action and around_action filter methods. Corresponding before_invocation and after_invocation methods should be used instead. #6275 [Kent Sibilev]
|
||||
|
||||
* Provide access to the underlying SOAP driver. #6212 [bmilekic, Kent Sibilev]
|
||||
|
||||
* Deprecation: update docs. #5998 [jakob@mentalized.net, Kevin Clark]
|
||||
|
||||
* ActionWebService WSDL generation ignores HTTP_X_FORWARDED_HOST [Paul Butcher <paul@paulbutcher.com>]
|
||||
|
||||
* Tighten rescue clauses. #5985 [james@grayproductions.net]
|
||||
|
||||
* Fixed XMLRPC multicall when one of the called methods returns a struct object. [Kent Sibilev]
|
||||
|
||||
* Replace Reloadable with Reloadable::Deprecated. [Nicholas Seckar]
|
||||
|
||||
* Fix invoke_layered since api_method didn't declare :expects. Closes #4720. [Kevin Ballard <kevin@sb.org>, Kent Sibilev]
|
||||
|
||||
* Replace alias method chaining with Module#alias_method_chain. [Marcel Molina Jr.]
|
||||
|
||||
* Replace Ruby's deprecated append_features in favor of included. [Marcel Molina Jr.]
|
||||
|
||||
* Fix test database name typo. [Marcel Molina Jr.]
|
||||
|
||||
*1.1.2* (April 9th, 2006)
|
||||
|
||||
* Rely on Active Record 1.14.2
|
||||
|
||||
|
||||
*1.1.1* (April 6th, 2006)
|
||||
|
||||
* Do not convert driver options to strings (#4499)
|
||||
|
||||
|
||||
*1.1.0* (March 27th, 2006)
|
||||
|
||||
* Make ActiveWebService::Struct type reloadable
|
||||
|
||||
* Fix scaffolding action when one of the members of a structural type has date or time type
|
||||
|
||||
* Remove extra index hash when generating scaffold html for parameters of structural type #4374 [joe@mjg2.com]
|
||||
|
||||
* Fix Scaffold Fails with Struct as a Parameter #4363 [joe@mjg2.com]
|
||||
|
||||
* Fix soap type registration of multidimensional arrays (#4232)
|
||||
|
||||
* Fix that marshaler couldn't handle ActiveRecord models defined in a different namespace (#2392).
|
||||
|
||||
* Fix that marshaler couldn't handle structs with members of ActiveRecord type (#1889).
|
||||
|
||||
* Fix that marshaler couldn't handle nil values for inner structs (#3576).
|
||||
|
||||
* Fix that changes to ActiveWebService::API::Base required restarting of the server (#2390).
|
||||
|
||||
* Fix scaffolding for signatures with :date, :time and :base64 types (#3321, #2769, #2078).
|
||||
|
||||
* Fix for incorrect casting of TrueClass/FalseClass instances (#2633, #3421).
|
||||
|
||||
* Fix for incompatibility problems with SOAP4R 1.5.5 (#2553) [Kent Sibilev]
|
||||
|
||||
|
||||
*1.0.0* (December 13th, 2005)
|
||||
|
||||
* Become part of Rails 1.0
|
||||
|
||||
*0.9.4* (December 7th, 2005)
|
||||
|
||||
* Update from LGPL to MIT license as per Minero Aoki's permission. [Marcel Molina Jr.]
|
||||
|
||||
* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.]
|
||||
|
||||
* Fix that XML-RPC date/time values did not have well-defined behaviour (#2516, #2534). This fix has one caveat, in that we can't support pre-1970 dates from XML-RPC clients.
|
||||
|
||||
*0.9.3* (November 7th, 2005)
|
||||
|
||||
* Upgraded to Action Pack 1.11.0 and Active Record 1.13.0
|
||||
|
||||
|
||||
*0.9.2* (October 26th, 2005)
|
||||
|
||||
* Upgraded to Action Pack 1.10.2 and Active Record 1.12.2
|
||||
|
||||
|
||||
*0.9.1* (October 19th, 2005)
|
||||
|
||||
* Upgraded to Action Pack 1.10.1 and Active Record 1.12.1
|
||||
|
||||
|
||||
*0.9.0* (October 16th, 2005)
|
||||
|
||||
* Fix invalid XML request generation bug in test_invoke [Ken Barker]
|
||||
|
||||
* Add XML-RPC 'system.multicall' support #1941 [jbonnar]
|
||||
|
||||
* Fix duplicate XSD entries for custom types shared across delegated/layered services #1729 [Tyler Kovacs]
|
||||
|
||||
* Allow multiple invocations in the same test method #1720 [dkhawk]
|
||||
|
||||
* Added ActionWebService::API::Base.soap_client and ActionWebService::API::Base.xmlrpc_client helper methods to create the internal clients for an API, useful for testing from ./script/console
|
||||
|
||||
* ActionWebService now always returns UTF-8 responses.
|
||||
|
||||
|
||||
*0.8.1* (11 July, 2005)
|
||||
|
||||
* Fix scaffolding for Action Pack controller changes
|
||||
|
||||
|
||||
*0.8.0* (6 July, 2005)
|
||||
|
||||
* Fix WSDL generation by aliasing #inherited instead of trying to overwrite it, or the WSDL action may end up not being defined in the controller
|
||||
|
||||
* Add ActionController::Base.wsdl_namespace option, to allow overriding of the namespace used in generated WSDL and SOAP messages. This is equivalent to the [WebService(Namespace = "Value")] attribute in .NET.
|
||||
|
||||
* Add workaround for Ruby 1.8.3's SOAP4R changing the return value of SOAP::Mapping::Registry#find_mapped_soap_class #1414 [Shugo Maeda]
|
||||
|
||||
* Fix moduled controller URLs in WSDL, and add unit test to verify the generated URL #1428
|
||||
|
||||
* Fix scaffolding template paths, it was broken on Win32
|
||||
|
||||
* Fix that functional testing of :layered controllers failed when using the SOAP protocol
|
||||
|
||||
* Allow invocation filters in :direct controllers as well, as they have access to more information regarding the web service request than ActionPack filters
|
||||
|
||||
* Add support for a :base64 signature type #1272 [Shugo Maeda]
|
||||
|
||||
* Fix that boolean fields were not rendered correctly in scaffolding
|
||||
|
||||
* Fix that scaffolding was not working for :delegated dispatching
|
||||
|
||||
* Add support for structured types as input parameters to scaffolding, this should let one test the blogging APIs using scaffolding as well
|
||||
|
||||
* Fix that generated WSDL was not using relative_url_root for base URI #1210 [Shugo Maeda]
|
||||
|
||||
* Use UTF-8 encoding by default for SOAP responses, but if an encoding is supplied by caller, use that for the response #1211 [Shugo Maeda, NAKAMURA Hiroshi]
|
||||
|
||||
* If the WSDL was retrieved over HTTPS, use HTTPS URLs in the WSDL too
|
||||
|
||||
* Fix that casting change in 0.7.0 would convert nil values to the default value for the type instead of leaving it as nil
|
||||
|
||||
|
||||
*0.7.1* (20th April, 2005)
|
||||
|
||||
* Depend on Active Record 1.10.1 and Action Pack 1.8.1
|
||||
|
||||
|
||||
*0.7.0* (19th April, 2005)
|
||||
|
||||
* When casting structured types, don't try to send obj.name= unless obj responds to it, causes casting to be less likely to fail for XML-RPC
|
||||
|
||||
* Add scaffolding via ActionController::Base.web_service_scaffold for quick testing using a web browser
|
||||
|
||||
* ActionWebService::API::Base#api_methods now returns a hash containing ActionWebService::API::Method objects instead of hashes. However, ActionWebService::API::Method defines a #[]() backwards compatibility method so any existing code utilizing this will still work.
|
||||
|
||||
* The :layered dispatching mode can now be used with SOAP as well, allowing you to support SOAP and XML-RPC clients for APIs like the metaWeblog API
|
||||
|
||||
* Remove ActiveRecordSoapMarshallable workaround, see #912 for details
|
||||
|
||||
* Generalize casting code to be used by both SOAP and XML-RPC (previously, it was only XML-RPC)
|
||||
|
||||
* Ensure return value is properly cast as well, fixes XML-RPC interoperability with Ecto and possibly other clients
|
||||
|
||||
* Include backtraces in 500 error responses for failed request parsing, and remove "rescue nil" statements obscuring real errors for XML-RPC
|
||||
|
||||
* Perform casting of struct members even if the structure is already of the correct type, so that the type we specify for the struct member is always the type of the value seen by the API implementation
|
||||
|
||||
|
||||
*0.6.2* (27th March, 2005)
|
||||
|
||||
* Allow method declarations for direct dispatching to declare parameters as well. We treat an arity of < 0 or > 0 as an indication that we should send through parameters. Closes #939.
|
||||
|
||||
|
||||
*0.6.1* (22th March, 2005)
|
||||
|
||||
* Fix that method response QNames mismatched with that declared in the WSDL, makes SOAP::WSDLDriverFactory work against AWS again
|
||||
|
||||
* Fix that @request.env was being modified, instead, dup the value gotten from env
|
||||
|
||||
* Fix XML-RPC example to use :layered mode, so it works again
|
||||
|
||||
* Support casting '0' or 0 into false, and '1' or 1 into true, when expecting a boolean value
|
||||
|
||||
* Fix that SOAP fault response fault code values were not QName's #804
|
||||
|
||||
|
||||
*0.6.0* (7th March, 2005)
|
||||
|
||||
* Add action_controller/test_invoke, used for integrating AWS with the Rails testing infrastructure
|
||||
|
||||
* Allow passing through options to the SOAP RPC driver for the SOAP client
|
||||
|
||||
* Make the SOAP WS marshaler use #columns to decide which fields to marshal as well, avoids providing attributes brought in by associations
|
||||
|
||||
* Add <tt>ActionWebService::API::Base.allow_active_record_expects</tt> option, with a default of false. Setting this to true will allow specifying ActiveRecord::Base model classes in <tt>:expects</tt>. API writers should take care to validate the received ActiveRecord model objects when turning it on, and/or have an authentication mechanism in place to reduce the security risk.
|
||||
|
||||
* Improve error message reporting. Bugs in either AWS or the web service itself will send back a protocol-specific error report message if possible, otherwise, provide as much detail as possible.
|
||||
|
||||
* Removed type checking of received parameters, and perform casting for XML-RPC if possible, but fallback to the received parameters if casting fails, closes #677
|
||||
|
||||
* Refactored SOAP and XML-RPC marshaling and encoding into a small library devoted exclusively to protocol specifics, also cleaned up the SOAP marshaling approach, so that array and custom type marshaling should be a bit faster.
|
||||
|
||||
* Add namespaced XML-RPC method name support, closes #678
|
||||
|
||||
* Replace '::' with '..' in fully qualified type names for marshaling and WSDL. This improves interoperability with .NET, and closes #676.
|
||||
|
||||
|
||||
*0.5.0* (24th February, 2005)
|
||||
|
||||
* lib/action_service/dispatcher*: replace "router" fragments with
|
||||
one file for Action Controllers, moves dispatching work out of
|
||||
the container
|
||||
* lib/*,test/*,examples/*: rename project to
|
||||
ActionWebService. prefix all generic "service" type names with web_.
|
||||
update all using code as well as the RDoc.
|
||||
* lib/action_service/router/wsdl.rb: ensure that #wsdl is
|
||||
defined in the final container class, or the new ActionPack
|
||||
filtering will exclude it
|
||||
* lib/action_service/struct.rb,test/struct_test.rb: create a
|
||||
default #initialize on inherit that accepts a Hash containing
|
||||
the default member values
|
||||
* lib/action_service/api/action_controller.rb: add support and
|
||||
tests for #client_api in controller
|
||||
* test/router_wsdl_test.rb: add tests to ensure declared
|
||||
service names don't contain ':', as ':' causes interoperability
|
||||
issues
|
||||
* lib/*, test/*: rename "interface" concept to "api", and change all
|
||||
related uses to reflect this change. update all uses of Inflector
|
||||
to call the method on String instead.
|
||||
* test/api_test.rb: add test to ensure API definition not
|
||||
instantiatable
|
||||
* lib/action_service/invocation.rb: change @invocation_params to
|
||||
@method_params
|
||||
* lib/*: update RDoc
|
||||
* lib/action_service/struct.rb: update to support base types
|
||||
* lib/action_service/support/signature.rb: support the notion of
|
||||
"base types" in signatures, with well-known unambiguous names such as :int,
|
||||
:bool, etc, which map to the correct Ruby class. accept the same names
|
||||
used by ActiveRecord as well as longer versions of each, as aliases.
|
||||
* examples/*: update for seperate API definition updates
|
||||
* lib/action_service/*, test/*: extensive refactoring: define API methods in
|
||||
a seperate class, and specify it wherever used with 'service_api'.
|
||||
this makes writing a client API for accessing defined API methods
|
||||
with ActionWebService really easy.
|
||||
* lib/action_service/container.rb: fix a bug in default call
|
||||
handling for direct dispatching, and add ActionController filter
|
||||
support for direct dispatching.
|
||||
* test/router_action_controller_test.rb: add tests to ensure
|
||||
ActionController filters are actually called.
|
||||
* test/protocol_soap_test.rb: add more tests for direct dispatching.
|
||||
|
||||
0.3.0
|
||||
|
||||
* First public release
|
||||
-364
@@ -1,364 +0,0 @@
|
||||
= Action Web Service -- Serving APIs on rails
|
||||
|
||||
Action Web Service provides a way to publish interoperable web service APIs with
|
||||
Rails without spending a lot of time delving into protocol details.
|
||||
|
||||
|
||||
== Features
|
||||
|
||||
* SOAP RPC protocol support
|
||||
* Dynamic WSDL generation for APIs
|
||||
* XML-RPC protocol support
|
||||
* Clients that use the same API definitions as the server for
|
||||
easy interoperability with other Action Web Service based applications
|
||||
* Type signature hints to improve interoperability with static languages
|
||||
* Active Record model class support in signatures
|
||||
|
||||
|
||||
== Defining your APIs
|
||||
|
||||
You specify the methods you want to make available as API methods in an
|
||||
ActionWebService::API::Base derivative, and then specify this API
|
||||
definition class wherever you want to use that API.
|
||||
|
||||
The implementation of the methods is done separately from the API
|
||||
specification.
|
||||
|
||||
|
||||
==== Method name inflection
|
||||
|
||||
Action Web Service will camelcase the method names according to Rails Inflector
|
||||
rules for the API visible to public callers. What this means, for example,
|
||||
is that the method names in generated WSDL will be camelcased, and callers will
|
||||
have to supply the camelcased name in their requests for the request to
|
||||
succeed.
|
||||
|
||||
If you do not desire this behaviour, you can turn it off with the
|
||||
ActionWebService::API::Base +inflect_names+ option.
|
||||
|
||||
|
||||
==== Inflection examples
|
||||
|
||||
:add => Add
|
||||
:find_all => FindAll
|
||||
|
||||
|
||||
==== Disabling inflection
|
||||
|
||||
class PersonAPI < ActionWebService::API::Base
|
||||
inflect_names false
|
||||
end
|
||||
|
||||
|
||||
==== API definition example
|
||||
|
||||
class PersonAPI < ActionWebService::API::Base
|
||||
api_method :add, :expects => [:string, :string, :bool], :returns => [:int]
|
||||
api_method :remove, :expects => [:int], :returns => [:bool]
|
||||
end
|
||||
|
||||
==== API usage example
|
||||
|
||||
class PersonController < ActionController::Base
|
||||
web_service_api PersonAPI
|
||||
|
||||
def add
|
||||
end
|
||||
|
||||
def remove
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
== Publishing your APIs
|
||||
|
||||
Action Web Service uses Action Pack to process protocol requests. There are two
|
||||
modes of dispatching protocol requests, _Direct_, and _Delegated_.
|
||||
|
||||
|
||||
=== Direct dispatching
|
||||
|
||||
This is the default mode. In this mode, public controller instance methods
|
||||
implement the API methods, and parameters are passed through to the methods in
|
||||
accordance with the API specification.
|
||||
|
||||
The return value of the method is sent back as the return value to the
|
||||
caller.
|
||||
|
||||
In this mode, a special <tt>api</tt> action is generated in the target
|
||||
controller to unwrap the protocol request, forward it on to the relevant method
|
||||
and send back the wrapped return value. <em>This action must not be
|
||||
overridden.</em>
|
||||
|
||||
==== Direct dispatching example
|
||||
|
||||
class PersonController < ApplicationController
|
||||
web_service_api PersonAPI
|
||||
|
||||
def add
|
||||
end
|
||||
|
||||
def remove
|
||||
end
|
||||
end
|
||||
|
||||
class PersonAPI < ActionWebService::API::Base
|
||||
...
|
||||
end
|
||||
|
||||
|
||||
For this example, protocol requests for +Add+ and +Remove+ methods sent to
|
||||
<tt>/person/api</tt> will be routed to the controller methods +add+ and +remove+.
|
||||
|
||||
|
||||
=== Delegated dispatching
|
||||
|
||||
This mode can be turned on by setting the +web_service_dispatching_mode+ option
|
||||
in a controller to <tt>:delegated</tt>.
|
||||
|
||||
In this mode, the controller contains one or more web service objects (objects
|
||||
that implement an ActionWebService::API::Base definition). These web service
|
||||
objects are each mapped onto one controller action only.
|
||||
|
||||
==== Delegated dispatching example
|
||||
|
||||
class ApiController < ApplicationController
|
||||
web_service_dispatching_mode :delegated
|
||||
|
||||
web_service :person, PersonService.new
|
||||
end
|
||||
|
||||
class PersonService < ActionWebService::Base
|
||||
web_service_api PersonAPI
|
||||
|
||||
def add
|
||||
end
|
||||
|
||||
def remove
|
||||
end
|
||||
end
|
||||
|
||||
class PersonAPI < ActionWebService::API::Base
|
||||
...
|
||||
end
|
||||
|
||||
|
||||
For this example, all protocol requests for +PersonService+ are
|
||||
sent to the <tt>/api/person</tt> action.
|
||||
|
||||
The <tt>/api/person</tt> action is generated when the +web_service+
|
||||
method is called. <em>This action must not be overridden.</em>
|
||||
|
||||
Other controller actions (actions that aren't the target of a +web_service+ call)
|
||||
are ignored for ActionWebService purposes, and can do normal action tasks.
|
||||
|
||||
|
||||
=== Layered dispatching
|
||||
|
||||
This mode can be turned on by setting the +web_service_dispatching_mode+ option
|
||||
in a controller to <tt>:layered</tt>.
|
||||
|
||||
This mode is similar to _delegated_ mode, in that multiple web service objects
|
||||
can be attached to one controller, however, all protocol requests are sent to a
|
||||
single endpoint.
|
||||
|
||||
Use this mode when you want to share code between XML-RPC and SOAP clients,
|
||||
for APIs where the XML-RPC method names have prefixes. An example of such
|
||||
a method name would be <tt>blogger.newPost</tt>.
|
||||
|
||||
|
||||
==== Layered dispatching example
|
||||
|
||||
|
||||
class ApiController < ApplicationController
|
||||
web_service_dispatching_mode :layered
|
||||
|
||||
web_service :mt, MovableTypeService.new
|
||||
web_service :blogger, BloggerService.new
|
||||
web_service :metaWeblog, MetaWeblogService.new
|
||||
end
|
||||
|
||||
class MovableTypeService < ActionWebService::Base
|
||||
...
|
||||
end
|
||||
|
||||
class BloggerService < ActionWebService::Base
|
||||
...
|
||||
end
|
||||
|
||||
class MetaWeblogService < ActionWebService::API::Base
|
||||
...
|
||||
end
|
||||
|
||||
|
||||
For this example, an XML-RPC call for a method with a name like
|
||||
<tt>mt.getCategories</tt> will be sent to the <tt>getCategories</tt>
|
||||
method on the <tt>:mt</tt> service.
|
||||
|
||||
|
||||
== Customizing WSDL generation
|
||||
|
||||
You can customize the names used for the SOAP bindings in the generated
|
||||
WSDL by using the wsdl_service_name option in a controller:
|
||||
|
||||
class WsController < ApplicationController
|
||||
wsdl_service_name 'MyApp'
|
||||
end
|
||||
|
||||
You can also customize the namespace used in the generated WSDL for
|
||||
custom types and message definition types:
|
||||
|
||||
class WsController < ApplicationController
|
||||
wsdl_namespace 'http://my.company.com/app/wsapi'
|
||||
end
|
||||
|
||||
The default namespace used is 'urn:ActionWebService', if you don't supply
|
||||
one.
|
||||
|
||||
|
||||
== ActionWebService and UTF-8
|
||||
|
||||
If you're going to be sending back strings containing non-ASCII UTF-8
|
||||
characters using the <tt>:string</tt> data type, you need to make sure that
|
||||
Ruby is using UTF-8 as the default encoding for its strings.
|
||||
|
||||
The default in Ruby is to use US-ASCII encoding for strings, which causes a string
|
||||
validation check in the Ruby SOAP library to fail and your string to be sent
|
||||
back as a Base-64 value, which may confuse clients that expected strings
|
||||
because of the WSDL.
|
||||
|
||||
Two ways of setting the default string encoding are:
|
||||
|
||||
* Start Ruby using the <tt>-Ku</tt> command-line option to the Ruby executable
|
||||
* Set the <tt>$KCODE</tt> flag in <tt>config/environment.rb</tt> to the
|
||||
string <tt>'UTF8'</tt>
|
||||
|
||||
|
||||
== Testing your APIs
|
||||
|
||||
|
||||
=== Functional testing
|
||||
|
||||
You can perform testing of your APIs by creating a functional test for the
|
||||
controller dispatching the API, and calling #invoke in the test case to
|
||||
perform the invocation.
|
||||
|
||||
Example:
|
||||
|
||||
class PersonApiControllerTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@controller = PersonController.new
|
||||
@request = ActionController::TestRequest.new
|
||||
@response = ActionController::TestResponse.new
|
||||
end
|
||||
|
||||
def test_add
|
||||
result = invoke :remove, 1
|
||||
assert_equal true, result
|
||||
end
|
||||
end
|
||||
|
||||
This example invokes the API method <tt>test</tt>, defined on
|
||||
the PersonController, and returns the result.
|
||||
|
||||
|
||||
=== Scaffolding
|
||||
|
||||
You can also test your APIs with a web browser by attaching scaffolding
|
||||
to the controller.
|
||||
|
||||
Example:
|
||||
|
||||
class PersonController
|
||||
web_service_scaffold :invocation
|
||||
end
|
||||
|
||||
This creates an action named <tt>invocation</tt> on the PersonController.
|
||||
|
||||
Navigating to this action lets you select the method to invoke, supply the parameters,
|
||||
and view the result of the invocation.
|
||||
|
||||
|
||||
== Using the client support
|
||||
|
||||
Action Web Service includes client classes that can use the same API
|
||||
definition as the server. The advantage of this approach is that your client
|
||||
will have the same support for Active Record and structured types as the
|
||||
server, and can just use them directly, and rely on the marshaling to Do The
|
||||
Right Thing.
|
||||
|
||||
*Note*: The client support is intended for communication between Ruby on Rails
|
||||
applications that both use Action Web Service. It may work with other servers, but
|
||||
that is not its intended use, and interoperability can't be guaranteed, especially
|
||||
not for .NET web services.
|
||||
|
||||
Web services protocol specifications are complex, and Action Web Service client
|
||||
support can only be guaranteed to work with a subset.
|
||||
|
||||
|
||||
==== Factory created client example
|
||||
|
||||
class BlogManagerController < ApplicationController
|
||||
web_client_api :blogger, :xmlrpc, 'http://url/to/blog/api/RPC2', :handler_name => 'blogger'
|
||||
end
|
||||
|
||||
class SearchingController < ApplicationController
|
||||
web_client_api :google, :soap, 'http://url/to/blog/api/beta', :service_name => 'GoogleSearch'
|
||||
end
|
||||
|
||||
See ActionWebService::API::ActionController::ClassMethods for more details.
|
||||
|
||||
==== Manually created client example
|
||||
|
||||
class PersonAPI < ActionWebService::API::Base
|
||||
api_method :find_all, :returns => [[Person]]
|
||||
end
|
||||
|
||||
soap_client = ActionWebService::Client::Soap.new(PersonAPI, "http://...")
|
||||
persons = soap_client.find_all
|
||||
|
||||
class BloggerAPI < ActionWebService::API::Base
|
||||
inflect_names false
|
||||
api_method :getRecentPosts, :returns => [[Blog::Post]]
|
||||
end
|
||||
|
||||
blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../xmlrpc", :handler_name => "blogger")
|
||||
posts = blog.getRecentPosts
|
||||
|
||||
|
||||
See ActionWebService::Client::Soap and ActionWebService::Client::XmlRpc for more details.
|
||||
|
||||
== Dependencies
|
||||
|
||||
Action Web Service requires that the Action Pack and Active Record are either
|
||||
available to be required immediately or are accessible as GEMs.
|
||||
|
||||
It also requires a version of Ruby that includes SOAP support in the standard
|
||||
library. At least version 1.8.2 final (2004-12-25) of Ruby is recommended; this
|
||||
is the version tested against.
|
||||
|
||||
|
||||
== Download
|
||||
|
||||
The latest Action Web Service version can be downloaded from
|
||||
http://rubyforge.org/projects/actionservice
|
||||
|
||||
|
||||
== Installation
|
||||
|
||||
You can install Action Web Service with the following command.
|
||||
|
||||
% [sudo] ruby setup.rb
|
||||
|
||||
|
||||
== License
|
||||
|
||||
Action Web Service is released under the MIT license.
|
||||
|
||||
|
||||
== Support
|
||||
|
||||
The Ruby on Rails mailing list
|
||||
|
||||
Or, to contact the author, send mail to bitserf@gmail.com
|
||||
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
require 'rubygems'
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
require 'rake/packagetask'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rake/contrib/rubyforgepublisher'
|
||||
require 'fileutils'
|
||||
require File.join(File.dirname(__FILE__), 'lib', 'action_web_service', 'version')
|
||||
|
||||
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
||||
PKG_NAME = 'actionwebservice'
|
||||
PKG_VERSION = ActionWebService::VERSION::STRING + PKG_BUILD
|
||||
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||
PKG_DESTINATION = ENV["RAILS_PKG_DESTINATION"] || "../#{PKG_NAME}"
|
||||
|
||||
RELEASE_NAME = "REL #{PKG_VERSION}"
|
||||
|
||||
RUBY_FORGE_PROJECT = "aws"
|
||||
RUBY_FORGE_USER = "webster132"
|
||||
|
||||
desc "Default Task"
|
||||
task :default => [ :test ]
|
||||
|
||||
|
||||
# Run the unit tests
|
||||
Rake::TestTask.new { |t|
|
||||
t.libs << "test"
|
||||
t.test_files = Dir['test/*_test.rb']
|
||||
t.verbose = true
|
||||
}
|
||||
|
||||
SCHEMA_PATH = File.join(File.dirname(__FILE__), *%w(test fixtures db_definitions))
|
||||
|
||||
desc 'Build the MySQL test database'
|
||||
task :build_database do
|
||||
%x( mysqladmin create actionwebservice_unittest )
|
||||
%x( mysql actionwebservice_unittest < #{File.join(SCHEMA_PATH, 'mysql.sql')} )
|
||||
end
|
||||
|
||||
|
||||
# Generate the RDoc documentation
|
||||
Rake::RDocTask.new { |rdoc|
|
||||
rdoc.rdoc_dir = 'doc'
|
||||
rdoc.title = "Action Web Service -- Web services for Action Pack"
|
||||
rdoc.options << '--line-numbers' << '--inline-source'
|
||||
rdoc.options << '--charset' << 'utf-8'
|
||||
rdoc.template = "#{ENV['template']}.rb" if ENV['template']
|
||||
rdoc.rdoc_files.include('README')
|
||||
rdoc.rdoc_files.include('CHANGELOG')
|
||||
rdoc.rdoc_files.include('lib/action_web_service.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/api/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/client/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/container/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/dispatcher/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/protocol/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/support/*.rb')
|
||||
}
|
||||
|
||||
|
||||
# Create compressed packages
|
||||
spec = Gem::Specification.new do |s|
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.name = PKG_NAME
|
||||
s.summary = "Web service support for Action Pack."
|
||||
s.description = %q{Adds WSDL/SOAP and XML-RPC web service support to Action Pack}
|
||||
s.version = PKG_VERSION
|
||||
|
||||
s.author = "Leon Breedt"
|
||||
s.email = "bitserf@gmail.com"
|
||||
s.rubyforge_project = "aws"
|
||||
s.homepage = "http://www.rubyonrails.org"
|
||||
|
||||
s.add_dependency('actionpack', '= 1.13.5' + PKG_BUILD)
|
||||
s.add_dependency('activerecord', '= 1.15.5' + PKG_BUILD)
|
||||
|
||||
s.has_rdoc = true
|
||||
s.requirements << 'none'
|
||||
s.require_path = 'lib'
|
||||
s.autorequire = 'action_web_service'
|
||||
|
||||
s.files = [ "Rakefile", "setup.rb", "README", "TODO", "CHANGELOG", "MIT-LICENSE" ]
|
||||
s.files = s.files + Dir.glob( "examples/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
end
|
||||
Rake::GemPackageTask.new(spec) do |p|
|
||||
p.gem_spec = spec
|
||||
p.need_tar = true
|
||||
p.need_zip = true
|
||||
end
|
||||
|
||||
|
||||
# Publish beta gem
|
||||
desc "Publish the API documentation"
|
||||
task :pgem => [:package] do
|
||||
Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
`ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'`
|
||||
end
|
||||
|
||||
# Publish documentation
|
||||
desc "Publish the API documentation"
|
||||
task :pdoc => [:rdoc] do
|
||||
Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/aws", "doc").upload
|
||||
end
|
||||
|
||||
|
||||
def each_source_file(*args)
|
||||
prefix, includes, excludes, open_file = args
|
||||
prefix ||= File.dirname(__FILE__)
|
||||
open_file = true if open_file.nil?
|
||||
includes ||= %w[lib\/action_web_service\.rb$ lib\/action_web_service\/.*\.rb$]
|
||||
excludes ||= %w[lib\/action_web_service\/vendor]
|
||||
Find.find(prefix) do |file_name|
|
||||
next if file_name =~ /\.svn/
|
||||
file_name.gsub!(/^\.\//, '')
|
||||
continue = false
|
||||
includes.each do |inc|
|
||||
if file_name.match(/#{inc}/)
|
||||
continue = true
|
||||
break
|
||||
end
|
||||
end
|
||||
next unless continue
|
||||
excludes.each do |exc|
|
||||
if file_name.match(/#{exc}/)
|
||||
continue = false
|
||||
break
|
||||
end
|
||||
end
|
||||
next unless continue
|
||||
if open_file
|
||||
File.open(file_name) do |f|
|
||||
yield file_name, f
|
||||
end
|
||||
else
|
||||
yield file_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Count lines of the AWS source code"
|
||||
task :lines do
|
||||
total_lines = total_loc = 0
|
||||
puts "Per File:"
|
||||
each_source_file do |file_name, f|
|
||||
file_lines = file_loc = 0
|
||||
while line = f.gets
|
||||
file_lines += 1
|
||||
next if line =~ /^\s*$/
|
||||
next if line =~ /^\s*#/
|
||||
file_loc += 1
|
||||
end
|
||||
puts " #{file_name}: Lines #{file_lines}, LOC #{file_loc}"
|
||||
total_lines += file_lines
|
||||
total_loc += file_loc
|
||||
end
|
||||
puts "Total:"
|
||||
puts " Lines #{total_lines}, LOC #{total_loc}"
|
||||
end
|
||||
|
||||
desc "Publish the release files to RubyForge."
|
||||
task :release => [ :package ] do
|
||||
require 'rubyforge'
|
||||
|
||||
packages = %w( gem tgz zip ).collect{ |ext| "pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" }
|
||||
|
||||
rubyforge = RubyForge.new
|
||||
rubyforge.login
|
||||
rubyforge.add_release(PKG_NAME, PKG_NAME, "REL #{PKG_VERSION}", *packages)
|
||||
end
|
||||
Vendored
-32
@@ -1,32 +0,0 @@
|
||||
= Post-1.0
|
||||
- Document/Literal SOAP support
|
||||
- URL-based dispatching, URL identifies method
|
||||
|
||||
- Add :rest dispatching mode, a.l.a. Backpack API. Clean up dispatching
|
||||
in general. Support vanilla XML-format as a "Rails" protocol?
|
||||
XML::Simple deserialization into params?
|
||||
|
||||
web_service_dispatching_mode :rest
|
||||
|
||||
def method1(params)
|
||||
end
|
||||
|
||||
def method2(params)
|
||||
end
|
||||
|
||||
|
||||
/ws/method1
|
||||
<xml>
|
||||
/ws/method2
|
||||
<yaml>
|
||||
|
||||
- Allow locking down a controller to only accept messages for a particular
|
||||
protocol. This will allow us to generate fully conformant error messages
|
||||
in cases where we currently fudge it if we don't know the protocol.
|
||||
|
||||
- Allow AWS user to participate in typecasting, so they can centralize
|
||||
workarounds for buggy input in one place
|
||||
|
||||
= Refactoring
|
||||
- Don't have clean way to go from SOAP Class object to the xsd:NAME type
|
||||
string -- NaHi possibly looking at remedying this situation
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
require 'action_web_service'
|
||||
|
||||
# These need to be in the load path for action_web_service to work
|
||||
Dependencies.load_paths += ["#{RAILS_ROOT}/app/apis"]
|
||||
|
||||
# AWS Test helpers
|
||||
require 'action_web_service/test_invoke' if ENV['RAILS_ENV'] && ENV['RAILS_ENV'] =~ /^test/
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
require 'rbconfig'
|
||||
require 'find'
|
||||
require 'ftools'
|
||||
|
||||
include Config
|
||||
|
||||
# this was adapted from rdoc's install.rb by way of Log4r
|
||||
|
||||
$sitedir = CONFIG["sitelibdir"]
|
||||
unless $sitedir
|
||||
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
|
||||
$libdir = File.join(CONFIG["libdir"], "ruby", version)
|
||||
$sitedir = $:.find {|x| x =~ /site_ruby/ }
|
||||
if !$sitedir
|
||||
$sitedir = File.join($libdir, "site_ruby")
|
||||
elsif $sitedir !~ Regexp.quote(version)
|
||||
$sitedir = File.join($sitedir, version)
|
||||
end
|
||||
end
|
||||
|
||||
# the actual gruntwork
|
||||
Dir.chdir("lib")
|
||||
|
||||
Find.find("action_web_service", "action_web_service.rb") { |f|
|
||||
if f[-3..-1] == ".rb"
|
||||
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
|
||||
else
|
||||
File::makedirs(File.join($sitedir, *f.split(/\//)))
|
||||
end
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
#--
|
||||
# Copyright (C) 2005 Leon Breedt
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
require 'action_controller'
|
||||
require 'active_record'
|
||||
rescue LoadError
|
||||
require 'rubygems'
|
||||
gem 'activesupport', '>= 1.0.2'
|
||||
gem 'actionpack', '>= 1.6.0'
|
||||
gem 'activerecord', '>= 1.9.0'
|
||||
end
|
||||
|
||||
$:.unshift(File.dirname(__FILE__) + "/action_web_service/vendor/")
|
||||
|
||||
require 'action_web_service/support/class_inheritable_options'
|
||||
require 'action_web_service/support/signature_types'
|
||||
require 'action_web_service/base'
|
||||
require 'action_web_service/client'
|
||||
require 'action_web_service/invocation'
|
||||
require 'action_web_service/api'
|
||||
require 'action_web_service/casting'
|
||||
require 'action_web_service/struct'
|
||||
require 'action_web_service/container'
|
||||
require 'action_web_service/protocol'
|
||||
require 'action_web_service/dispatcher'
|
||||
require 'action_web_service/scaffolding'
|
||||
|
||||
ActionWebService::Base.class_eval do
|
||||
include ActionWebService::Container::Direct
|
||||
include ActionWebService::Invocation
|
||||
end
|
||||
|
||||
ActionController::Base.class_eval do
|
||||
include ActionWebService::Protocol::Discovery
|
||||
include ActionWebService::Protocol::Soap
|
||||
include ActionWebService::Protocol::XmlRpc
|
||||
include ActionWebService::Container::Direct
|
||||
include ActionWebService::Container::Delegated
|
||||
include ActionWebService::Container::ActionController
|
||||
include ActionWebService::Invocation
|
||||
include ActionWebService::Dispatcher
|
||||
include ActionWebService::Dispatcher::ActionController
|
||||
include ActionWebService::Scaffolding
|
||||
end
|
||||
@@ -1,297 +0,0 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module API # :nodoc:
|
||||
# A web service API class specifies the methods that will be available for
|
||||
# invocation for an API. It also contains metadata such as the method type
|
||||
# signature hints.
|
||||
#
|
||||
# It is not intended to be instantiated.
|
||||
#
|
||||
# It is attached to web service implementation classes like
|
||||
# ActionWebService::Base and ActionController::Base derivatives by using
|
||||
# <tt>container.web_service_api</tt>, where <tt>container</tt> is an
|
||||
# ActionController::Base or a ActionWebService::Base.
|
||||
#
|
||||
# See ActionWebService::Container::Direct::ClassMethods for an example
|
||||
# of use.
|
||||
class Base
|
||||
# Whether to transform the public API method names into camel-cased names
|
||||
class_inheritable_option :inflect_names, true
|
||||
|
||||
# By default only HTTP POST requests are processed
|
||||
class_inheritable_option :allowed_http_methods, [ :post ]
|
||||
|
||||
# Whether to allow ActiveRecord::Base models in <tt>:expects</tt>.
|
||||
# The default is +false+; you should be aware of the security implications
|
||||
# of allowing this, and ensure that you don't allow remote callers to
|
||||
# easily overwrite data they should not have access to.
|
||||
class_inheritable_option :allow_active_record_expects, false
|
||||
|
||||
# If present, the name of a method to call when the remote caller
|
||||
# tried to call a nonexistent method. Semantically equivalent to
|
||||
# +method_missing+.
|
||||
class_inheritable_option :default_api_method
|
||||
|
||||
# Disallow instantiation
|
||||
private_class_method :new, :allocate
|
||||
|
||||
class << self
|
||||
include ActionWebService::SignatureTypes
|
||||
|
||||
# API methods have a +name+, which must be the Ruby method name to use when
|
||||
# performing the invocation on the web service object.
|
||||
#
|
||||
# The signatures for the method input parameters and return value can
|
||||
# by specified in +options+.
|
||||
#
|
||||
# A signature is an array of one or more parameter specifiers.
|
||||
# A parameter specifier can be one of the following:
|
||||
#
|
||||
# * A symbol or string representing one of the Action Web Service base types.
|
||||
# See ActionWebService::SignatureTypes for a canonical list of the base types.
|
||||
# * The Class object of the parameter type
|
||||
# * A single-element Array containing one of the two preceding items. This
|
||||
# will cause Action Web Service to treat the parameter at that position
|
||||
# as an array containing only values of the given type.
|
||||
# * A Hash containing as key the name of the parameter, and as value
|
||||
# one of the three preceding items
|
||||
#
|
||||
# If no method input parameter or method return value signatures are given,
|
||||
# the method is assumed to take no parameters and/or return no values of
|
||||
# interest, and any values that are received by the server will be
|
||||
# discarded and ignored.
|
||||
#
|
||||
# Valid options:
|
||||
# [<tt>:expects</tt>] Signature for the method input parameters
|
||||
# [<tt>:returns</tt>] Signature for the method return value
|
||||
# [<tt>:expects_and_returns</tt>] Signature for both input parameters and return value
|
||||
def api_method(name, options={})
|
||||
unless options.is_a?(Hash)
|
||||
raise(ActionWebServiceError, "Expected a Hash for options")
|
||||
end
|
||||
validate_options([:expects, :returns, :expects_and_returns], options.keys)
|
||||
if options[:expects_and_returns]
|
||||
expects = options[:expects_and_returns]
|
||||
returns = options[:expects_and_returns]
|
||||
else
|
||||
expects = options[:expects]
|
||||
returns = options[:returns]
|
||||
end
|
||||
expects = canonical_signature(expects)
|
||||
returns = canonical_signature(returns)
|
||||
if expects
|
||||
expects.each do |type|
|
||||
type = type.element_type if type.is_a?(ArrayType)
|
||||
if type.type_class.ancestors.include?(ActiveRecord::Base) && !allow_active_record_expects
|
||||
raise(ActionWebServiceError, "ActiveRecord model classes not allowed in :expects")
|
||||
end
|
||||
end
|
||||
end
|
||||
name = name.to_sym
|
||||
public_name = public_api_method_name(name)
|
||||
method = Method.new(name, public_name, expects, returns)
|
||||
write_inheritable_hash("api_methods", name => method)
|
||||
write_inheritable_hash("api_public_method_names", public_name => name)
|
||||
end
|
||||
|
||||
# Whether the given method name is a service method on this API
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.has_api_method?('GetCount') #=> false
|
||||
# ProjectsApi.has_api_method?(:getCount) #=> true
|
||||
def has_api_method?(name)
|
||||
api_methods.has_key?(name)
|
||||
end
|
||||
|
||||
# Whether the given public method name has a corresponding service method
|
||||
# on this API
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.has_api_method?(:getCount) #=> false
|
||||
# ProjectsApi.has_api_method?('GetCount') #=> true
|
||||
def has_public_api_method?(public_name)
|
||||
api_public_method_names.has_key?(public_name)
|
||||
end
|
||||
|
||||
# The corresponding public method name for the given service method name
|
||||
#
|
||||
# ProjectsApi.public_api_method_name('GetCount') #=> "GetCount"
|
||||
# ProjectsApi.public_api_method_name(:getCount) #=> "GetCount"
|
||||
def public_api_method_name(name)
|
||||
if inflect_names
|
||||
name.to_s.camelize
|
||||
else
|
||||
name.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# The corresponding service method name for the given public method name
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.api_method_name('GetCount') #=> :getCount
|
||||
def api_method_name(public_name)
|
||||
api_public_method_names[public_name]
|
||||
end
|
||||
|
||||
# A Hash containing all service methods on this API, and their
|
||||
# associated metadata.
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# api_method :getCompletedCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.api_methods #=>
|
||||
# {:getCount=>#<ActionWebService::API::Method:0x24379d8 ...>,
|
||||
# :getCompletedCount=>#<ActionWebService::API::Method:0x2437794 ...>}
|
||||
# ProjectsApi.api_methods[:getCount].public_name #=> "GetCount"
|
||||
def api_methods
|
||||
read_inheritable_attribute("api_methods") || {}
|
||||
end
|
||||
|
||||
# The Method instance for the given public API method name, if any
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# api_method :getCompletedCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.public_api_method_instance('GetCount') #=> <#<ActionWebService::API::Method:0x24379d8 ...>
|
||||
# ProjectsApi.public_api_method_instance(:getCount) #=> nil
|
||||
def public_api_method_instance(public_method_name)
|
||||
api_method_instance(api_method_name(public_method_name))
|
||||
end
|
||||
|
||||
# The Method instance for the given API method name, if any
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# api_method :getCompletedCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.api_method_instance(:getCount) #=> <ActionWebService::API::Method:0x24379d8 ...>
|
||||
# ProjectsApi.api_method_instance('GetCount') #=> <ActionWebService::API::Method:0x24379d8 ...>
|
||||
def api_method_instance(method_name)
|
||||
api_methods[method_name]
|
||||
end
|
||||
|
||||
# The Method instance for the default API method, if any
|
||||
def default_api_method_instance
|
||||
return nil unless name = default_api_method
|
||||
instance = read_inheritable_attribute("default_api_method_instance")
|
||||
if instance && instance.name == name
|
||||
return instance
|
||||
end
|
||||
instance = Method.new(name, public_api_method_name(name), nil, nil)
|
||||
write_inheritable_attribute("default_api_method_instance", instance)
|
||||
instance
|
||||
end
|
||||
|
||||
private
|
||||
def api_public_method_names
|
||||
read_inheritable_attribute("api_public_method_names") || {}
|
||||
end
|
||||
|
||||
def validate_options(valid_option_keys, supplied_option_keys)
|
||||
unknown_option_keys = supplied_option_keys - valid_option_keys
|
||||
unless unknown_option_keys.empty?
|
||||
raise(ActionWebServiceError, "Unknown options: #{unknown_option_keys}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Represents an API method and its associated metadata, and provides functionality
|
||||
# to assist in commonly performed API method tasks.
|
||||
class Method
|
||||
attr :name
|
||||
attr :public_name
|
||||
attr :expects
|
||||
attr :returns
|
||||
|
||||
def initialize(name, public_name, expects, returns)
|
||||
@name = name
|
||||
@public_name = public_name
|
||||
@expects = expects
|
||||
@returns = returns
|
||||
@caster = ActionWebService::Casting::BaseCaster.new(self)
|
||||
end
|
||||
|
||||
# The list of parameter names for this method
|
||||
def param_names
|
||||
return [] unless @expects
|
||||
@expects.map{ |type| type.name }
|
||||
end
|
||||
|
||||
# Casts a set of Ruby values into the expected Ruby values
|
||||
def cast_expects(params)
|
||||
@caster.cast_expects(params)
|
||||
end
|
||||
|
||||
# Cast a Ruby return value into the expected Ruby value
|
||||
def cast_returns(return_value)
|
||||
@caster.cast_returns(return_value)
|
||||
end
|
||||
|
||||
# Returns the index of the first expected parameter
|
||||
# with the given name
|
||||
def expects_index_of(param_name)
|
||||
return -1 if @expects.nil?
|
||||
(0..(@expects.length-1)).each do |i|
|
||||
return i if @expects[i].name.to_s == param_name.to_s
|
||||
end
|
||||
-1
|
||||
end
|
||||
|
||||
# Returns a hash keyed by parameter name for the given
|
||||
# parameter list
|
||||
def expects_to_hash(params)
|
||||
return {} if @expects.nil?
|
||||
h = {}
|
||||
@expects.zip(params){ |type, param| h[type.name] = param }
|
||||
h
|
||||
end
|
||||
|
||||
# Backwards compatibility with previous API
|
||||
def [](sig_type)
|
||||
case sig_type
|
||||
when :expects
|
||||
@expects.map{|x| compat_signature_entry(x)}
|
||||
when :returns
|
||||
@returns.map{|x| compat_signature_entry(x)}
|
||||
end
|
||||
end
|
||||
|
||||
# String representation of this method
|
||||
def to_s
|
||||
fqn = ""
|
||||
fqn << (@returns ? (@returns[0].human_name(false) + " ") : "void ")
|
||||
fqn << "#{@public_name}("
|
||||
fqn << @expects.map{ |p| p.human_name }.join(", ") if @expects
|
||||
fqn << ")"
|
||||
fqn
|
||||
end
|
||||
|
||||
private
|
||||
def compat_signature_entry(entry)
|
||||
if entry.array?
|
||||
[compat_signature_entry(entry.element_type)]
|
||||
else
|
||||
if entry.spec.is_a?(Hash)
|
||||
{entry.spec.keys.first => entry.type_class}
|
||||
else
|
||||
entry.type_class
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
module ActionWebService # :nodoc:
|
||||
class ActionWebServiceError < StandardError # :nodoc:
|
||||
end
|
||||
|
||||
# An Action Web Service object implements a specified API.
|
||||
#
|
||||
# Used by controllers operating in _Delegated_ dispatching mode.
|
||||
#
|
||||
# ==== Example
|
||||
#
|
||||
# class PersonService < ActionWebService::Base
|
||||
# web_service_api PersonAPI
|
||||
#
|
||||
# def find_person(criteria)
|
||||
# Person.find(:all) [...]
|
||||
# end
|
||||
#
|
||||
# def delete_person(id)
|
||||
# Person.find_by_id(id).destroy
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class PersonAPI < ActionWebService::API::Base
|
||||
# api_method :find_person, :expects => [SearchCriteria], :returns => [[Person]]
|
||||
# api_method :delete_person, :expects => [:int]
|
||||
# end
|
||||
#
|
||||
# class SearchCriteria < ActionWebService::Struct
|
||||
# member :firstname, :string
|
||||
# member :lastname, :string
|
||||
# member :email, :string
|
||||
# end
|
||||
class Base
|
||||
# Whether to report exceptions back to the caller in the protocol's exception
|
||||
# format
|
||||
class_inheritable_option :web_service_exception_reporting, true
|
||||
end
|
||||
end
|
||||
@@ -1,138 +0,0 @@
|
||||
require 'time'
|
||||
require 'date'
|
||||
require 'xmlrpc/datetime'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module Casting # :nodoc:
|
||||
class CastingError < ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
# Performs casting of arbitrary values into the correct types for the signature
|
||||
class BaseCaster # :nodoc:
|
||||
def initialize(api_method)
|
||||
@api_method = api_method
|
||||
end
|
||||
|
||||
# Coerces the parameters in +params+ (an Enumerable) into the types
|
||||
# this method expects
|
||||
def cast_expects(params)
|
||||
self.class.cast_expects(@api_method, params)
|
||||
end
|
||||
|
||||
# Coerces the given +return_value+ into the type returned by this
|
||||
# method
|
||||
def cast_returns(return_value)
|
||||
self.class.cast_returns(@api_method, return_value)
|
||||
end
|
||||
|
||||
class << self
|
||||
include ActionWebService::SignatureTypes
|
||||
|
||||
def cast_expects(api_method, params) # :nodoc:
|
||||
return [] if api_method.expects.nil?
|
||||
api_method.expects.zip(params).map{ |type, param| cast(param, type) }
|
||||
end
|
||||
|
||||
def cast_returns(api_method, return_value) # :nodoc:
|
||||
return nil if api_method.returns.nil?
|
||||
cast(return_value, api_method.returns[0])
|
||||
end
|
||||
|
||||
def cast(value, signature_type) # :nodoc:
|
||||
return value if signature_type.nil? # signature.length != params.length
|
||||
return nil if value.nil?
|
||||
# XMLRPC protocol doesn't support nil values. It uses false instead.
|
||||
# It should never happen for SOAP.
|
||||
if signature_type.structured? && value.equal?(false)
|
||||
return nil
|
||||
end
|
||||
unless signature_type.array? || signature_type.structured?
|
||||
return value if canonical_type(value.class) == signature_type.type
|
||||
end
|
||||
if signature_type.array?
|
||||
unless value.respond_to?(:entries) && !value.is_a?(String)
|
||||
raise CastingError, "Don't know how to cast #{value.class} into #{signature_type.type.inspect}"
|
||||
end
|
||||
value.entries.map do |entry|
|
||||
cast(entry, signature_type.element_type)
|
||||
end
|
||||
elsif signature_type.structured?
|
||||
cast_to_structured_type(value, signature_type)
|
||||
elsif !signature_type.custom?
|
||||
cast_base_type(value, signature_type)
|
||||
end
|
||||
end
|
||||
|
||||
def cast_base_type(value, signature_type) # :nodoc:
|
||||
# This is a work-around for the fact that XML-RPC special-cases DateTime values into its own DateTime type
|
||||
# in order to support iso8601 dates. This doesn't work too well for us, so we'll convert it into a Time,
|
||||
# with the caveat that we won't be able to handle pre-1970 dates that are sent to us.
|
||||
#
|
||||
# See http://dev.rubyonrails.com/ticket/2516
|
||||
value = value.to_time if value.is_a?(XMLRPC::DateTime)
|
||||
|
||||
case signature_type.type
|
||||
when :int
|
||||
Integer(value)
|
||||
when :string
|
||||
value.to_s
|
||||
when :base64
|
||||
if value.is_a?(ActionWebService::Base64)
|
||||
value
|
||||
else
|
||||
ActionWebService::Base64.new(value.to_s)
|
||||
end
|
||||
when :bool
|
||||
return false if value.nil?
|
||||
return value if value == true || value == false
|
||||
case value.to_s.downcase
|
||||
when '1', 'true', 'y', 'yes'
|
||||
true
|
||||
when '0', 'false', 'n', 'no'
|
||||
false
|
||||
else
|
||||
raise CastingError, "Don't know how to cast #{value.class} into Boolean"
|
||||
end
|
||||
when :float
|
||||
Float(value)
|
||||
when :decimal
|
||||
BigDecimal(value.to_s)
|
||||
when :time
|
||||
value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash)
|
||||
value.kind_of?(Time) ? value : Time.parse(value.to_s)
|
||||
when :date
|
||||
value = "%s/%s/%s" % value.values_at(*%w[2 3 1]) if value.kind_of?(Hash)
|
||||
value.kind_of?(Date) ? value : Date.parse(value.to_s)
|
||||
when :datetime
|
||||
value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash)
|
||||
value.kind_of?(DateTime) ? value : DateTime.parse(value.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
def cast_to_structured_type(value, signature_type) # :nodoc:
|
||||
obj = nil
|
||||
obj = value if canonical_type(value.class) == canonical_type(signature_type.type)
|
||||
obj ||= signature_type.type_class.new
|
||||
if value.respond_to?(:each_pair)
|
||||
klass = signature_type.type_class
|
||||
value.each_pair do |name, val|
|
||||
type = klass.respond_to?(:member_type) ? klass.member_type(name) : nil
|
||||
val = cast(val, type) if type
|
||||
# See http://dev.rubyonrails.com/ticket/3567
|
||||
val = val.to_time if val.is_a?(XMLRPC::DateTime)
|
||||
obj.__send__("#{name}=", val) if obj.respond_to?(name)
|
||||
end
|
||||
elsif value.respond_to?(:attributes)
|
||||
signature_type.each_member do |name, type|
|
||||
val = value.__send__(name)
|
||||
obj.__send__("#{name}=", cast(val, type)) if obj.respond_to?(name)
|
||||
end
|
||||
else
|
||||
raise CastingError, "Don't know how to cast #{value.class} to #{signature_type.type_class}"
|
||||
end
|
||||
obj
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
require 'action_web_service/client/base'
|
||||
require 'action_web_service/client/soap_client'
|
||||
require 'action_web_service/client/xmlrpc_client'
|
||||
@@ -1,28 +0,0 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Client # :nodoc:
|
||||
class ClientError < StandardError # :nodoc:
|
||||
end
|
||||
|
||||
class Base # :nodoc:
|
||||
def initialize(api, endpoint_uri)
|
||||
@api = api
|
||||
@endpoint_uri = endpoint_uri
|
||||
end
|
||||
|
||||
def method_missing(name, *args) # :nodoc:
|
||||
call_name = method_name(name)
|
||||
return super(name, *args) if call_name.nil?
|
||||
self.perform_invocation(call_name, args)
|
||||
end
|
||||
|
||||
private
|
||||
def method_name(name)
|
||||
if @api.has_api_method?(name.to_sym)
|
||||
name.to_s
|
||||
elsif @api.has_public_api_method?(name.to_s)
|
||||
@api.api_method_name(name.to_s).to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
require 'soap/rpc/driver'
|
||||
require 'uri'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module Client # :nodoc:
|
||||
|
||||
# Implements SOAP client support (using RPC encoding for the messages).
|
||||
#
|
||||
# ==== Example Usage
|
||||
#
|
||||
# class PersonAPI < ActionWebService::API::Base
|
||||
# api_method :find_all, :returns => [[Person]]
|
||||
# end
|
||||
#
|
||||
# soap_client = ActionWebService::Client::Soap.new(PersonAPI, "http://...")
|
||||
# persons = soap_client.find_all
|
||||
#
|
||||
class Soap < Base
|
||||
# provides access to the underlying soap driver
|
||||
attr_reader :driver
|
||||
|
||||
# Creates a new web service client using the SOAP RPC protocol.
|
||||
#
|
||||
# +api+ must be an ActionWebService::API::Base derivative, and
|
||||
# +endpoint_uri+ must point at the relevant URL to which protocol requests
|
||||
# will be sent with HTTP POST.
|
||||
#
|
||||
# Valid options:
|
||||
# [<tt>:namespace</tt>] If the remote server has used a custom namespace to
|
||||
# declare its custom types, you can specify it here. This would
|
||||
# be the namespace declared with a [WebService(Namespace = "http://namespace")] attribute
|
||||
# in .NET, for example.
|
||||
# [<tt>:driver_options</tt>] If you want to supply any custom SOAP RPC driver
|
||||
# options, you can provide them as a Hash here
|
||||
#
|
||||
# The <tt>:driver_options</tt> option can be used to configure the backend SOAP
|
||||
# RPC driver. An example of configuring the SOAP backend to do
|
||||
# client-certificate authenticated SSL connections to the server:
|
||||
#
|
||||
# opts = {}
|
||||
# opts['protocol.http.ssl_config.verify_mode'] = 'OpenSSL::SSL::VERIFY_PEER'
|
||||
# opts['protocol.http.ssl_config.client_cert'] = client_cert_file_path
|
||||
# opts['protocol.http.ssl_config.client_key'] = client_key_file_path
|
||||
# opts['protocol.http.ssl_config.ca_file'] = ca_cert_file_path
|
||||
# client = ActionWebService::Client::Soap.new(api, 'https://some/service', :driver_options => opts)
|
||||
def initialize(api, endpoint_uri, options={})
|
||||
super(api, endpoint_uri)
|
||||
@namespace = options[:namespace] || 'urn:ActionWebService'
|
||||
@driver_options = options[:driver_options] || {}
|
||||
@protocol = ActionWebService::Protocol::Soap::SoapProtocol.new @namespace
|
||||
@soap_action_base = options[:soap_action_base]
|
||||
@soap_action_base ||= URI.parse(endpoint_uri).path
|
||||
@driver = create_soap_rpc_driver(api, endpoint_uri)
|
||||
@driver_options.each do |name, value|
|
||||
@driver.options[name.to_s] = value
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def perform_invocation(method_name, args)
|
||||
method = @api.api_methods[method_name.to_sym]
|
||||
args = method.cast_expects(args.dup) rescue args
|
||||
return_value = @driver.send(method_name, *args)
|
||||
method.cast_returns(return_value.dup) rescue return_value
|
||||
end
|
||||
|
||||
def soap_action(method_name)
|
||||
"#{@soap_action_base}/#{method_name}"
|
||||
end
|
||||
|
||||
private
|
||||
def create_soap_rpc_driver(api, endpoint_uri)
|
||||
@protocol.register_api(api)
|
||||
driver = SoapDriver.new(endpoint_uri, nil)
|
||||
driver.mapping_registry = @protocol.marshaler.registry
|
||||
api.api_methods.each do |name, method|
|
||||
qname = XSD::QName.new(@namespace, method.public_name)
|
||||
action = soap_action(method.public_name)
|
||||
expects = method.expects
|
||||
returns = method.returns
|
||||
param_def = []
|
||||
if expects
|
||||
expects.each do |type|
|
||||
type_binding = @protocol.marshaler.lookup_type(type)
|
||||
if SOAP::Version >= "1.5.5"
|
||||
param_def << ['in', type.name.to_s, [type_binding.type.type_class.to_s]]
|
||||
else
|
||||
param_def << ['in', type.name, type_binding.mapping]
|
||||
end
|
||||
end
|
||||
end
|
||||
if returns
|
||||
type_binding = @protocol.marshaler.lookup_type(returns[0])
|
||||
if SOAP::Version >= "1.5.5"
|
||||
param_def << ['retval', 'return', [type_binding.type.type_class.to_s]]
|
||||
else
|
||||
param_def << ['retval', 'return', type_binding.mapping]
|
||||
end
|
||||
end
|
||||
driver.add_method(qname, action, method.name.to_s, param_def)
|
||||
end
|
||||
driver
|
||||
end
|
||||
|
||||
class SoapDriver < SOAP::RPC::Driver # :nodoc:
|
||||
def add_method(qname, soapaction, name, param_def)
|
||||
@proxy.add_rpc_method(qname, soapaction, name, param_def)
|
||||
add_rpc_method_interface(name, param_def)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
require 'uri'
|
||||
require 'xmlrpc/client'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module Client # :nodoc:
|
||||
|
||||
# Implements XML-RPC client support
|
||||
#
|
||||
# ==== Example Usage
|
||||
#
|
||||
# class BloggerAPI < ActionWebService::API::Base
|
||||
# inflect_names false
|
||||
# api_method :getRecentPosts, :returns => [[Blog::Post]]
|
||||
# end
|
||||
#
|
||||
# blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../RPC", :handler_name => "blogger")
|
||||
# posts = blog.getRecentPosts
|
||||
class XmlRpc < Base
|
||||
|
||||
# Creates a new web service client using the XML-RPC protocol.
|
||||
#
|
||||
# +api+ must be an ActionWebService::API::Base derivative, and
|
||||
# +endpoint_uri+ must point at the relevant URL to which protocol requests
|
||||
# will be sent with HTTP POST.
|
||||
#
|
||||
# Valid options:
|
||||
# [<tt>:handler_name</tt>] If the remote server defines its services inside special
|
||||
# handler (the Blogger API uses a <tt>"blogger"</tt> handler name for example),
|
||||
# provide it here, or your method calls will fail
|
||||
def initialize(api, endpoint_uri, options={})
|
||||
@api = api
|
||||
@handler_name = options[:handler_name]
|
||||
@protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.new
|
||||
@client = XMLRPC::Client.new2(endpoint_uri, options[:proxy], options[:timeout])
|
||||
end
|
||||
|
||||
protected
|
||||
def perform_invocation(method_name, args)
|
||||
method = @api.api_methods[method_name.to_sym]
|
||||
if method.expects && method.expects.length != args.length
|
||||
raise(ArgumentError, "#{method.public_name}: wrong number of arguments (#{args.length} for #{method.expects.length})")
|
||||
end
|
||||
args = method.cast_expects(args.dup) rescue args
|
||||
if method.expects
|
||||
method.expects.each_with_index{ |type, i| args[i] = @protocol.value_to_xmlrpc_wire_format(args[i], type) }
|
||||
end
|
||||
ok, return_value = @client.call2(public_name(method_name), *args)
|
||||
return (method.cast_returns(return_value.dup) rescue return_value) if ok
|
||||
raise(ClientError, "#{return_value.faultCode}: #{return_value.faultString}")
|
||||
end
|
||||
|
||||
def public_name(method_name)
|
||||
public_name = @api.public_api_method_name(method_name)
|
||||
@handler_name ? "#{@handler_name}.#{public_name}" : public_name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
require 'action_web_service/container/direct_container'
|
||||
require 'action_web_service/container/delegated_container'
|
||||
require 'action_web_service/container/action_controller_container'
|
||||
Vendored
-93
@@ -1,93 +0,0 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Container # :nodoc:
|
||||
module ActionController # :nodoc:
|
||||
def self.included(base) # :nodoc:
|
||||
class << base
|
||||
include ClassMethods
|
||||
alias_method_chain :inherited, :api
|
||||
alias_method_chain :web_service_api, :require
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Creates a client for accessing remote web services, using the
|
||||
# given +protocol+ to communicate with the +endpoint_uri+.
|
||||
#
|
||||
# ==== Example
|
||||
#
|
||||
# class MyController < ActionController::Base
|
||||
# web_client_api :blogger, :xmlrpc, "http://blogger.com/myblog/api/RPC2", :handler_name => 'blogger'
|
||||
# end
|
||||
#
|
||||
# In this example, a protected method named <tt>blogger</tt> will
|
||||
# now exist on the controller, and calling it will return the
|
||||
# XML-RPC client object for working with that remote service.
|
||||
#
|
||||
# +options+ is the set of protocol client specific options (see
|
||||
# a protocol client class for details).
|
||||
#
|
||||
# If your API definition does not exist on the load path with the
|
||||
# correct rules for it to be found using +name+, you can pass in
|
||||
# the API definition class via +options+, using a key of <tt>:api</tt>
|
||||
def web_client_api(name, protocol, endpoint_uri, options={})
|
||||
unless method_defined?(name)
|
||||
api_klass = options.delete(:api) || require_web_service_api(name)
|
||||
class_eval do
|
||||
define_method(name) do
|
||||
create_web_service_client(api_klass, protocol, endpoint_uri, options)
|
||||
end
|
||||
protected name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def web_service_api_with_require(definition=nil) # :nodoc:
|
||||
return web_service_api_without_require if definition.nil?
|
||||
case definition
|
||||
when String, Symbol
|
||||
klass = require_web_service_api(definition)
|
||||
else
|
||||
klass = definition
|
||||
end
|
||||
web_service_api_without_require(klass)
|
||||
end
|
||||
|
||||
def require_web_service_api(name) # :nodoc:
|
||||
case name
|
||||
when String, Symbol
|
||||
file_name = name.to_s.underscore + "_api"
|
||||
class_name = file_name.camelize
|
||||
class_names = [class_name, class_name.sub(/Api$/, 'API')]
|
||||
begin
|
||||
require_dependency(file_name)
|
||||
rescue LoadError => load_error
|
||||
requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
|
||||
msg = requiree == file_name ? "Missing API definition file in apis/#{file_name}.rb" : "Can't load file: #{requiree}"
|
||||
raise LoadError.new(msg).copy_blame!(load_error)
|
||||
end
|
||||
klass = nil
|
||||
class_names.each do |name|
|
||||
klass = name.constantize rescue nil
|
||||
break unless klass.nil?
|
||||
end
|
||||
unless klass
|
||||
raise(NameError, "neither #{class_names[0]} or #{class_names[1]} found")
|
||||
end
|
||||
klass
|
||||
else
|
||||
raise(ArgumentError, "expected String or Symbol argument")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def inherited_with_api(child)
|
||||
inherited_without_api(child)
|
||||
begin child.web_service_api(child.controller_path)
|
||||
rescue MissingSourceFile => e
|
||||
raise unless e.is_missing?("apis/#{child.controller_path}_api")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Container # :nodoc:
|
||||
module Delegated # :nodoc:
|
||||
class ContainerError < ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
base.send(:include, ActionWebService::Container::Delegated::InstanceMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Declares a web service that will provide access to the API of the given
|
||||
# +object+. +object+ must be an ActionWebService::Base derivative.
|
||||
#
|
||||
# Web service object creation can either be _immediate_, where the object
|
||||
# instance is given at class definition time, or _deferred_, where
|
||||
# object instantiation is delayed until request time.
|
||||
#
|
||||
# ==== Immediate web service object example
|
||||
#
|
||||
# class ApiController < ApplicationController
|
||||
# web_service_dispatching_mode :delegated
|
||||
#
|
||||
# web_service :person, PersonService.new
|
||||
# end
|
||||
#
|
||||
# For deferred instantiation, a block should be given instead of an
|
||||
# object instance. This block will be executed in controller instance
|
||||
# context, so it can rely on controller instance variables being present.
|
||||
#
|
||||
# ==== Deferred web service object example
|
||||
#
|
||||
# class ApiController < ApplicationController
|
||||
# web_service_dispatching_mode :delegated
|
||||
#
|
||||
# web_service(:person) { PersonService.new(request.env) }
|
||||
# end
|
||||
def web_service(name, object=nil, &block)
|
||||
if (object && block_given?) || (object.nil? && block.nil?)
|
||||
raise(ContainerError, "either service, or a block must be given")
|
||||
end
|
||||
name = name.to_sym
|
||||
if block_given?
|
||||
info = { name => { :block => block } }
|
||||
else
|
||||
info = { name => { :object => object } }
|
||||
end
|
||||
write_inheritable_hash("web_services", info)
|
||||
call_web_service_definition_callbacks(self, name, info)
|
||||
end
|
||||
|
||||
# Whether this service contains a service with the given +name+
|
||||
def has_web_service?(name)
|
||||
web_services.has_key?(name.to_sym)
|
||||
end
|
||||
|
||||
def web_services # :nodoc:
|
||||
read_inheritable_attribute("web_services") || {}
|
||||
end
|
||||
|
||||
def add_web_service_definition_callback(&block) # :nodoc:
|
||||
write_inheritable_array("web_service_definition_callbacks", [block])
|
||||
end
|
||||
|
||||
private
|
||||
def call_web_service_definition_callbacks(container_class, web_service_name, service_info)
|
||||
(read_inheritable_attribute("web_service_definition_callbacks") || []).each do |block|
|
||||
block.call(container_class, web_service_name, service_info)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
def web_service_object(web_service_name)
|
||||
info = self.class.web_services[web_service_name.to_sym]
|
||||
unless info
|
||||
raise(ContainerError, "no such web service '#{web_service_name}'")
|
||||
end
|
||||
service = info[:block]
|
||||
service ? self.instance_eval(&service) : info[:object]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Container # :nodoc:
|
||||
module Direct # :nodoc:
|
||||
class ContainerError < ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Attaches ActionWebService API +definition+ to the calling class.
|
||||
#
|
||||
# Action Controllers can have a default associated API, removing the need
|
||||
# to call this method if you follow the Action Web Service naming conventions.
|
||||
#
|
||||
# A controller with a class name of GoogleSearchController will
|
||||
# implicitly load <tt>app/apis/google_search_api.rb</tt>, and expect the
|
||||
# API definition class to be named <tt>GoogleSearchAPI</tt> or
|
||||
# <tt>GoogleSearchApi</tt>.
|
||||
#
|
||||
# ==== Service class example
|
||||
#
|
||||
# class MyService < ActionWebService::Base
|
||||
# web_service_api MyAPI
|
||||
# end
|
||||
#
|
||||
# class MyAPI < ActionWebService::API::Base
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# ==== Controller class example
|
||||
#
|
||||
# class MyController < ActionController::Base
|
||||
# web_service_api MyAPI
|
||||
# end
|
||||
#
|
||||
# class MyAPI < ActionWebService::API::Base
|
||||
# ...
|
||||
# end
|
||||
def web_service_api(definition=nil)
|
||||
if definition.nil?
|
||||
read_inheritable_attribute("web_service_api")
|
||||
else
|
||||
if definition.is_a?(Symbol)
|
||||
raise(ContainerError, "symbols can only be used for #web_service_api inside of a controller")
|
||||
end
|
||||
unless definition.respond_to?(:ancestors) && definition.ancestors.include?(ActionWebService::API::Base)
|
||||
raise(ContainerError, "#{definition.to_s} is not a valid API definition")
|
||||
end
|
||||
write_inheritable_attribute("web_service_api", definition)
|
||||
call_web_service_api_callbacks(self, definition)
|
||||
end
|
||||
end
|
||||
|
||||
def add_web_service_api_callback(&block) # :nodoc:
|
||||
write_inheritable_array("web_service_api_callbacks", [block])
|
||||
end
|
||||
|
||||
private
|
||||
def call_web_service_api_callbacks(container_class, definition)
|
||||
(read_inheritable_attribute("web_service_api_callbacks") || []).each do |block|
|
||||
block.call(container_class, definition)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
require 'action_web_service/dispatcher/abstract'
|
||||
require 'action_web_service/dispatcher/action_controller_dispatcher'
|
||||
-207
@@ -1,207 +0,0 @@
|
||||
require 'benchmark'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module Dispatcher # :nodoc:
|
||||
class DispatcherError < ActionWebService::ActionWebServiceError # :nodoc:
|
||||
def initialize(*args)
|
||||
super
|
||||
set_backtrace(caller)
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.class_inheritable_option(:web_service_dispatching_mode, :direct)
|
||||
base.class_inheritable_option(:web_service_exception_reporting, true)
|
||||
base.send(:include, ActionWebService::Dispatcher::InstanceMethods)
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
private
|
||||
def invoke_web_service_request(protocol_request)
|
||||
invocation = web_service_invocation(protocol_request)
|
||||
if invocation.is_a?(Array) && protocol_request.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol)
|
||||
xmlrpc_multicall_invoke(invocation)
|
||||
else
|
||||
web_service_invoke(invocation)
|
||||
end
|
||||
end
|
||||
|
||||
def web_service_direct_invoke(invocation)
|
||||
@method_params = invocation.method_ordered_params
|
||||
arity = method(invocation.api_method.name).arity rescue 0
|
||||
if arity < 0 || arity > 0
|
||||
params = @method_params
|
||||
else
|
||||
params = []
|
||||
end
|
||||
web_service_filtered_invoke(invocation, params)
|
||||
end
|
||||
|
||||
def web_service_delegated_invoke(invocation)
|
||||
web_service_filtered_invoke(invocation, invocation.method_ordered_params)
|
||||
end
|
||||
|
||||
def web_service_filtered_invoke(invocation, params)
|
||||
cancellation_reason = nil
|
||||
return_value = invocation.service.perform_invocation(invocation.api_method.name, params) do |x|
|
||||
cancellation_reason = x
|
||||
end
|
||||
if cancellation_reason
|
||||
raise(DispatcherError, "request canceled: #{cancellation_reason}")
|
||||
end
|
||||
return_value
|
||||
end
|
||||
|
||||
def web_service_invoke(invocation)
|
||||
case web_service_dispatching_mode
|
||||
when :direct
|
||||
return_value = web_service_direct_invoke(invocation)
|
||||
when :delegated, :layered
|
||||
return_value = web_service_delegated_invoke(invocation)
|
||||
end
|
||||
web_service_create_response(invocation.protocol, invocation.protocol_options, invocation.api, invocation.api_method, return_value)
|
||||
end
|
||||
|
||||
def xmlrpc_multicall_invoke(invocations)
|
||||
responses = []
|
||||
invocations.each do |invocation|
|
||||
if invocation.is_a?(Hash)
|
||||
responses << [invocation, nil]
|
||||
next
|
||||
end
|
||||
begin
|
||||
case web_service_dispatching_mode
|
||||
when :direct
|
||||
return_value = web_service_direct_invoke(invocation)
|
||||
when :delegated, :layered
|
||||
return_value = web_service_delegated_invoke(invocation)
|
||||
end
|
||||
api_method = invocation.api_method
|
||||
if invocation.api.has_api_method?(api_method.name)
|
||||
response_type = (api_method.returns ? api_method.returns[0] : nil)
|
||||
return_value = api_method.cast_returns(return_value)
|
||||
else
|
||||
response_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0)
|
||||
end
|
||||
responses << [return_value, response_type]
|
||||
rescue Exception => e
|
||||
responses << [{ 'faultCode' => 3, 'faultString' => e.message }, nil]
|
||||
end
|
||||
end
|
||||
invocation = invocations[0]
|
||||
invocation.protocol.encode_multicall_response(responses, invocation.protocol_options)
|
||||
end
|
||||
|
||||
def web_service_invocation(request, level = 0)
|
||||
public_method_name = request.method_name
|
||||
invocation = Invocation.new
|
||||
invocation.protocol = request.protocol
|
||||
invocation.protocol_options = request.protocol_options
|
||||
invocation.service_name = request.service_name
|
||||
if web_service_dispatching_mode == :layered
|
||||
case invocation.protocol
|
||||
when Protocol::Soap::SoapProtocol
|
||||
soap_action = request.protocol_options[:soap_action]
|
||||
if soap_action && soap_action =~ /^\/\w+\/(\w+)\//
|
||||
invocation.service_name = $1
|
||||
end
|
||||
when Protocol::XmlRpc::XmlRpcProtocol
|
||||
if request.method_name =~ /^([^\.]+)\.(.*)$/
|
||||
public_method_name = $2
|
||||
invocation.service_name = $1
|
||||
end
|
||||
end
|
||||
end
|
||||
if invocation.protocol.is_a? Protocol::XmlRpc::XmlRpcProtocol
|
||||
if public_method_name == 'multicall' && invocation.service_name == 'system'
|
||||
if level > 0
|
||||
raise(DispatcherError, "Recursive system.multicall invocations not allowed")
|
||||
end
|
||||
multicall = request.method_params.dup
|
||||
unless multicall.is_a?(Array) && multicall[0].is_a?(Array)
|
||||
raise(DispatcherError, "Malformed multicall (expected array of Hash elements)")
|
||||
end
|
||||
multicall = multicall[0]
|
||||
return multicall.map do |item|
|
||||
raise(DispatcherError, "Multicall elements must be Hash") unless item.is_a?(Hash)
|
||||
raise(DispatcherError, "Multicall elements must contain a 'methodName' key") unless item.has_key?('methodName')
|
||||
method_name = item['methodName']
|
||||
params = item.has_key?('params') ? item['params'] : []
|
||||
multicall_request = request.dup
|
||||
multicall_request.method_name = method_name
|
||||
multicall_request.method_params = params
|
||||
begin
|
||||
web_service_invocation(multicall_request, level + 1)
|
||||
rescue Exception => e
|
||||
{'faultCode' => 4, 'faultMessage' => e.message}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
case web_service_dispatching_mode
|
||||
when :direct
|
||||
invocation.api = self.class.web_service_api
|
||||
invocation.service = self
|
||||
when :delegated, :layered
|
||||
invocation.service = web_service_object(invocation.service_name)
|
||||
invocation.api = invocation.service.class.web_service_api
|
||||
end
|
||||
if invocation.api.nil?
|
||||
raise(DispatcherError, "no API attached to #{invocation.service.class}")
|
||||
end
|
||||
invocation.protocol.register_api(invocation.api)
|
||||
request.api = invocation.api
|
||||
if invocation.api.has_public_api_method?(public_method_name)
|
||||
invocation.api_method = invocation.api.public_api_method_instance(public_method_name)
|
||||
else
|
||||
if invocation.api.default_api_method.nil?
|
||||
raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api}")
|
||||
else
|
||||
invocation.api_method = invocation.api.default_api_method_instance
|
||||
end
|
||||
end
|
||||
if invocation.service.nil?
|
||||
raise(DispatcherError, "no service available for service name #{invocation.service_name}")
|
||||
end
|
||||
unless invocation.service.respond_to?(invocation.api_method.name)
|
||||
raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api} (#{invocation.api_method.name})")
|
||||
end
|
||||
request.api_method = invocation.api_method
|
||||
begin
|
||||
invocation.method_ordered_params = invocation.api_method.cast_expects(request.method_params.dup)
|
||||
rescue
|
||||
logger.warn "Casting of method parameters failed" unless logger.nil?
|
||||
invocation.method_ordered_params = request.method_params
|
||||
end
|
||||
request.method_params = invocation.method_ordered_params
|
||||
invocation.method_named_params = {}
|
||||
invocation.api_method.param_names.inject(0) do |m, n|
|
||||
invocation.method_named_params[n] = invocation.method_ordered_params[m]
|
||||
m + 1
|
||||
end
|
||||
invocation
|
||||
end
|
||||
|
||||
def web_service_create_response(protocol, protocol_options, api, api_method, return_value)
|
||||
if api.has_api_method?(api_method.name)
|
||||
return_type = api_method.returns ? api_method.returns[0] : nil
|
||||
return_value = api_method.cast_returns(return_value)
|
||||
else
|
||||
return_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0)
|
||||
end
|
||||
protocol.encode_response(api_method.public_name + 'Response', return_value, return_type, protocol_options)
|
||||
end
|
||||
|
||||
class Invocation # :nodoc:
|
||||
attr_accessor :protocol
|
||||
attr_accessor :protocol_options
|
||||
attr_accessor :service_name
|
||||
attr_accessor :api
|
||||
attr_accessor :api_method
|
||||
attr_accessor :method_ordered_params
|
||||
attr_accessor :method_named_params
|
||||
attr_accessor :service
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Vendored
-379
@@ -1,379 +0,0 @@
|
||||
require 'benchmark'
|
||||
require 'builder/xmlmarkup'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module Dispatcher # :nodoc:
|
||||
module ActionController # :nodoc:
|
||||
def self.included(base) # :nodoc:
|
||||
class << base
|
||||
include ClassMethods
|
||||
alias_method_chain :inherited, :action_controller
|
||||
end
|
||||
base.class_eval do
|
||||
alias_method :web_service_direct_invoke_without_controller, :web_service_direct_invoke
|
||||
end
|
||||
base.add_web_service_api_callback do |klass, api|
|
||||
if klass.web_service_dispatching_mode == :direct
|
||||
klass.class_eval 'def api; dispatch_web_service_request; end'
|
||||
end
|
||||
end
|
||||
base.add_web_service_definition_callback do |klass, name, info|
|
||||
if klass.web_service_dispatching_mode == :delegated
|
||||
klass.class_eval "def #{name}; dispatch_web_service_request; end"
|
||||
elsif klass.web_service_dispatching_mode == :layered
|
||||
klass.class_eval 'def api; dispatch_web_service_request; end'
|
||||
end
|
||||
end
|
||||
base.send(:include, ActionWebService::Dispatcher::ActionController::InstanceMethods)
|
||||
end
|
||||
|
||||
module ClassMethods # :nodoc:
|
||||
def inherited_with_action_controller(child)
|
||||
inherited_without_action_controller(child)
|
||||
child.send(:include, ActionWebService::Dispatcher::ActionController::WsdlAction)
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
private
|
||||
def dispatch_web_service_request
|
||||
method = request.method.to_s.upcase
|
||||
allowed_methods = self.class.web_service_api ? (self.class.web_service_api.allowed_http_methods || []) : [ :post ]
|
||||
allowed_methods = allowed_methods.map{|m| m.to_s.upcase }
|
||||
if !allowed_methods.include?(method)
|
||||
render :text => "#{method} not supported", :status=>500
|
||||
return
|
||||
end
|
||||
exception = nil
|
||||
begin
|
||||
ws_request = discover_web_service_request(request)
|
||||
rescue Exception => e
|
||||
exception = e
|
||||
end
|
||||
if ws_request
|
||||
ws_response = nil
|
||||
exception = nil
|
||||
bm = Benchmark.measure do
|
||||
begin
|
||||
ws_response = invoke_web_service_request(ws_request)
|
||||
rescue Exception => e
|
||||
exception = e
|
||||
end
|
||||
end
|
||||
log_request(ws_request, request.raw_post)
|
||||
if exception
|
||||
log_error(exception) unless logger.nil?
|
||||
send_web_service_error_response(ws_request, exception)
|
||||
else
|
||||
send_web_service_response(ws_response, bm.real)
|
||||
end
|
||||
else
|
||||
exception ||= DispatcherError.new("Malformed SOAP or XML-RPC protocol message")
|
||||
log_error(exception) unless logger.nil?
|
||||
send_web_service_error_response(ws_request, exception)
|
||||
end
|
||||
rescue Exception => e
|
||||
log_error(e) unless logger.nil?
|
||||
send_web_service_error_response(ws_request, e)
|
||||
end
|
||||
|
||||
def send_web_service_response(ws_response, elapsed=nil)
|
||||
log_response(ws_response, elapsed)
|
||||
options = { :type => ws_response.content_type, :disposition => 'inline' }
|
||||
send_data(ws_response.body, options)
|
||||
end
|
||||
|
||||
def send_web_service_error_response(ws_request, exception)
|
||||
if ws_request
|
||||
unless self.class.web_service_exception_reporting
|
||||
exception = DispatcherError.new("Internal server error (exception raised)")
|
||||
end
|
||||
api_method = ws_request.api_method
|
||||
public_method_name = api_method ? api_method.public_name : ws_request.method_name
|
||||
return_type = ActionWebService::SignatureTypes.canonical_signature_entry(Exception, 0)
|
||||
ws_response = ws_request.protocol.encode_response(public_method_name + 'Response', exception, return_type, ws_request.protocol_options)
|
||||
send_web_service_response(ws_response)
|
||||
else
|
||||
if self.class.web_service_exception_reporting
|
||||
message = exception.message
|
||||
backtrace = "\nBacktrace:\n#{exception.backtrace.join("\n")}"
|
||||
else
|
||||
message = "Exception raised"
|
||||
backtrace = ""
|
||||
end
|
||||
render :text => "Internal protocol error: #{message}#{backtrace}", :status => 500
|
||||
end
|
||||
end
|
||||
|
||||
def web_service_direct_invoke(invocation)
|
||||
invocation.method_named_params.each do |name, value|
|
||||
params[name] = value
|
||||
end
|
||||
web_service_direct_invoke_without_controller(invocation)
|
||||
end
|
||||
|
||||
def log_request(ws_request, body)
|
||||
unless logger.nil?
|
||||
name = ws_request.method_name
|
||||
api_method = ws_request.api_method
|
||||
params = ws_request.method_params
|
||||
if api_method && api_method.expects
|
||||
params = api_method.expects.zip(params).map{ |type, param| "#{type.name}=>#{param.inspect}" }
|
||||
else
|
||||
params = params.map{ |param| param.inspect }
|
||||
end
|
||||
service = ws_request.service_name
|
||||
logger.debug("\nWeb Service Request: #{name}(#{params.join(", ")}) Entrypoint: #{service}")
|
||||
logger.debug(indent(body))
|
||||
end
|
||||
end
|
||||
|
||||
def log_response(ws_response, elapsed=nil)
|
||||
unless logger.nil?
|
||||
elapsed = (elapsed ? " (%f):" % elapsed : ":")
|
||||
logger.debug("\nWeb Service Response" + elapsed + " => #{ws_response.return_value.inspect}")
|
||||
logger.debug(indent(ws_response.body))
|
||||
end
|
||||
end
|
||||
|
||||
def indent(body)
|
||||
body.split(/\n/).map{|x| " #{x}"}.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
module WsdlAction # :nodoc:
|
||||
XsdNs = 'http://www.w3.org/2001/XMLSchema'
|
||||
WsdlNs = 'http://schemas.xmlsoap.org/wsdl/'
|
||||
SoapNs = 'http://schemas.xmlsoap.org/wsdl/soap/'
|
||||
SoapEncodingNs = 'http://schemas.xmlsoap.org/soap/encoding/'
|
||||
SoapHttpTransport = 'http://schemas.xmlsoap.org/soap/http'
|
||||
|
||||
def wsdl
|
||||
case request.method
|
||||
when :get
|
||||
begin
|
||||
options = { :type => 'text/xml', :disposition => 'inline' }
|
||||
send_data(to_wsdl, options)
|
||||
rescue Exception => e
|
||||
log_error(e) unless logger.nil?
|
||||
end
|
||||
when :post
|
||||
render :text => 'POST not supported', :status => 500
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def base_uri
|
||||
host = request.host_with_port
|
||||
relative_url_root = request.relative_url_root
|
||||
scheme = request.ssl? ? 'https' : 'http'
|
||||
'%s://%s%s/%s/' % [scheme, host, relative_url_root, self.class.controller_path]
|
||||
end
|
||||
|
||||
def to_wsdl
|
||||
xml = ''
|
||||
dispatching_mode = web_service_dispatching_mode
|
||||
global_service_name = wsdl_service_name
|
||||
namespace = wsdl_namespace || 'urn:ActionWebService'
|
||||
soap_action_base = "/#{controller_name}"
|
||||
|
||||
marshaler = ActionWebService::Protocol::Soap::SoapMarshaler.new(namespace)
|
||||
apis = {}
|
||||
case dispatching_mode
|
||||
when :direct
|
||||
api = self.class.web_service_api
|
||||
web_service_name = controller_class_name.sub(/Controller$/, '').underscore
|
||||
apis[web_service_name] = [api, register_api(api, marshaler)]
|
||||
when :delegated, :layered
|
||||
self.class.web_services.each do |web_service_name, info|
|
||||
service = web_service_object(web_service_name)
|
||||
api = service.class.web_service_api
|
||||
apis[web_service_name] = [api, register_api(api, marshaler)]
|
||||
end
|
||||
end
|
||||
custom_types = []
|
||||
apis.values.each do |api, bindings|
|
||||
bindings.each do |b|
|
||||
custom_types << b unless custom_types.include?(b)
|
||||
end
|
||||
end
|
||||
|
||||
xm = Builder::XmlMarkup.new(:target => xml, :indent => 2)
|
||||
xm.instruct!
|
||||
xm.definitions('name' => wsdl_service_name,
|
||||
'targetNamespace' => namespace,
|
||||
'xmlns:typens' => namespace,
|
||||
'xmlns:xsd' => XsdNs,
|
||||
'xmlns:soap' => SoapNs,
|
||||
'xmlns:soapenc' => SoapEncodingNs,
|
||||
'xmlns:wsdl' => WsdlNs,
|
||||
'xmlns' => WsdlNs) do
|
||||
# Generate XSD
|
||||
if custom_types.size > 0
|
||||
xm.types do
|
||||
xm.xsd(:schema, 'xmlns' => XsdNs, 'targetNamespace' => namespace) do
|
||||
custom_types.each do |binding|
|
||||
case
|
||||
when binding.type.array?
|
||||
xm.xsd(:complexType, 'name' => binding.type_name) do
|
||||
xm.xsd(:complexContent) do
|
||||
xm.xsd(:restriction, 'base' => 'soapenc:Array') do
|
||||
xm.xsd(:attribute, 'ref' => 'soapenc:arrayType',
|
||||
'wsdl:arrayType' => binding.element_binding.qualified_type_name('typens') + '[]')
|
||||
end
|
||||
end
|
||||
end
|
||||
when binding.type.structured?
|
||||
xm.xsd(:complexType, 'name' => binding.type_name) do
|
||||
xm.xsd(:all) do
|
||||
binding.type.each_member do |name, type|
|
||||
b = marshaler.register_type(type)
|
||||
xm.xsd(:element, 'name' => name, 'type' => b.qualified_type_name('typens'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# APIs
|
||||
apis.each do |api_name, values|
|
||||
api = values[0]
|
||||
api.api_methods.each do |name, method|
|
||||
gen = lambda do |msg_name, direction|
|
||||
xm.message('name' => message_name_for(api_name, msg_name)) do
|
||||
sym = nil
|
||||
if direction == :out
|
||||
returns = method.returns
|
||||
if returns
|
||||
binding = marshaler.register_type(returns[0])
|
||||
xm.part('name' => 'return', 'type' => binding.qualified_type_name('typens'))
|
||||
end
|
||||
else
|
||||
expects = method.expects
|
||||
expects.each do |type|
|
||||
binding = marshaler.register_type(type)
|
||||
xm.part('name' => type.name, 'type' => binding.qualified_type_name('typens'))
|
||||
end if expects
|
||||
end
|
||||
end
|
||||
end
|
||||
public_name = method.public_name
|
||||
gen.call(public_name, :in)
|
||||
gen.call("#{public_name}Response", :out)
|
||||
end
|
||||
|
||||
# Port
|
||||
port_name = port_name_for(global_service_name, api_name)
|
||||
xm.portType('name' => port_name) do
|
||||
api.api_methods.each do |name, method|
|
||||
xm.operation('name' => method.public_name) do
|
||||
xm.input('message' => "typens:" + message_name_for(api_name, method.public_name))
|
||||
xm.output('message' => "typens:" + message_name_for(api_name, "#{method.public_name}Response"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Bind it
|
||||
binding_name = binding_name_for(global_service_name, api_name)
|
||||
xm.binding('name' => binding_name, 'type' => "typens:#{port_name}") do
|
||||
xm.soap(:binding, 'style' => 'rpc', 'transport' => SoapHttpTransport)
|
||||
api.api_methods.each do |name, method|
|
||||
xm.operation('name' => method.public_name) do
|
||||
case web_service_dispatching_mode
|
||||
when :direct
|
||||
soap_action = soap_action_base + "/api/" + method.public_name
|
||||
when :delegated, :layered
|
||||
soap_action = soap_action_base \
|
||||
+ "/" + api_name.to_s \
|
||||
+ "/" + method.public_name
|
||||
end
|
||||
xm.soap(:operation, 'soapAction' => soap_action)
|
||||
xm.input do
|
||||
xm.soap(:body,
|
||||
'use' => 'encoded',
|
||||
'namespace' => namespace,
|
||||
'encodingStyle' => SoapEncodingNs)
|
||||
end
|
||||
xm.output do
|
||||
xm.soap(:body,
|
||||
'use' => 'encoded',
|
||||
'namespace' => namespace,
|
||||
'encodingStyle' => SoapEncodingNs)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Define it
|
||||
xm.service('name' => "#{global_service_name}Service") do
|
||||
apis.each do |api_name, values|
|
||||
port_name = port_name_for(global_service_name, api_name)
|
||||
binding_name = binding_name_for(global_service_name, api_name)
|
||||
case web_service_dispatching_mode
|
||||
when :direct, :layered
|
||||
binding_target = 'api'
|
||||
when :delegated
|
||||
binding_target = api_name.to_s
|
||||
end
|
||||
xm.port('name' => port_name, 'binding' => "typens:#{binding_name}") do
|
||||
xm.soap(:address, 'location' => "#{base_uri}#{binding_target}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def port_name_for(global_service, service)
|
||||
"#{global_service}#{service.to_s.camelize}Port"
|
||||
end
|
||||
|
||||
def binding_name_for(global_service, service)
|
||||
"#{global_service}#{service.to_s.camelize}Binding"
|
||||
end
|
||||
|
||||
def message_name_for(api_name, message_name)
|
||||
mode = web_service_dispatching_mode
|
||||
if mode == :layered || mode == :delegated
|
||||
api_name.to_s + '-' + message_name
|
||||
else
|
||||
message_name
|
||||
end
|
||||
end
|
||||
|
||||
def register_api(api, marshaler)
|
||||
bindings = {}
|
||||
traverse_custom_types(api, marshaler, bindings) do |binding|
|
||||
bindings[binding] = nil unless bindings.has_key?(binding)
|
||||
element_binding = binding.element_binding
|
||||
bindings[element_binding] = nil if element_binding && !bindings.has_key?(element_binding)
|
||||
end
|
||||
bindings.keys
|
||||
end
|
||||
|
||||
def traverse_custom_types(api, marshaler, bindings, &block)
|
||||
api.api_methods.each do |name, method|
|
||||
expects, returns = method.expects, method.returns
|
||||
expects.each{ |type| traverse_type(marshaler, type, bindings, &block) if type.custom? } if expects
|
||||
returns.each{ |type| traverse_type(marshaler, type, bindings, &block) if type.custom? } if returns
|
||||
end
|
||||
end
|
||||
|
||||
def traverse_type(marshaler, type, bindings, &block)
|
||||
binding = marshaler.register_type(type)
|
||||
return if bindings.has_key?(binding)
|
||||
bindings[binding] = nil
|
||||
yield binding
|
||||
if type.array?
|
||||
yield marshaler.register_type(type.element_type)
|
||||
type = type.element_type
|
||||
end
|
||||
type.each_member{ |name, type| traverse_type(marshaler, type, bindings, &block) } if type.structured?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,202 +0,0 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Invocation # :nodoc:
|
||||
class InvocationError < ActionWebService::ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
base.send(:include, ActionWebService::Invocation::InstanceMethods)
|
||||
end
|
||||
|
||||
# Invocation interceptors provide a means to execute custom code before
|
||||
# and after method invocations on ActionWebService::Base objects.
|
||||
#
|
||||
# When running in _Direct_ dispatching mode, ActionController filters
|
||||
# should be used for this functionality instead.
|
||||
#
|
||||
# The semantics of invocation interceptors are the same as ActionController
|
||||
# filters, and accept the same parameters and options.
|
||||
#
|
||||
# A _before_ interceptor can also cancel execution by returning +false+,
|
||||
# or returning a <tt>[false, "cancel reason"]</tt> array if it wishes to supply
|
||||
# a reason for canceling the request.
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# class CustomService < ActionWebService::Base
|
||||
# before_invocation :intercept_add, :only => [:add]
|
||||
#
|
||||
# def add(a, b)
|
||||
# a + b
|
||||
# end
|
||||
#
|
||||
# private
|
||||
# def intercept_add
|
||||
# return [false, "permission denied"] # cancel it
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Options:
|
||||
# [<tt>:except</tt>] A list of methods for which the interceptor will NOT be called
|
||||
# [<tt>:only</tt>] A list of methods for which the interceptor WILL be called
|
||||
module ClassMethods
|
||||
# Appends the given +interceptors+ to be called
|
||||
# _before_ method invocation.
|
||||
def append_before_invocation(*interceptors, &block)
|
||||
conditions = extract_conditions!(interceptors)
|
||||
interceptors << block if block_given?
|
||||
add_interception_conditions(interceptors, conditions)
|
||||
append_interceptors_to_chain("before", interceptors)
|
||||
end
|
||||
|
||||
# Prepends the given +interceptors+ to be called
|
||||
# _before_ method invocation.
|
||||
def prepend_before_invocation(*interceptors, &block)
|
||||
conditions = extract_conditions!(interceptors)
|
||||
interceptors << block if block_given?
|
||||
add_interception_conditions(interceptors, conditions)
|
||||
prepend_interceptors_to_chain("before", interceptors)
|
||||
end
|
||||
|
||||
alias :before_invocation :append_before_invocation
|
||||
|
||||
# Appends the given +interceptors+ to be called
|
||||
# _after_ method invocation.
|
||||
def append_after_invocation(*interceptors, &block)
|
||||
conditions = extract_conditions!(interceptors)
|
||||
interceptors << block if block_given?
|
||||
add_interception_conditions(interceptors, conditions)
|
||||
append_interceptors_to_chain("after", interceptors)
|
||||
end
|
||||
|
||||
# Prepends the given +interceptors+ to be called
|
||||
# _after_ method invocation.
|
||||
def prepend_after_invocation(*interceptors, &block)
|
||||
conditions = extract_conditions!(interceptors)
|
||||
interceptors << block if block_given?
|
||||
add_interception_conditions(interceptors, conditions)
|
||||
prepend_interceptors_to_chain("after", interceptors)
|
||||
end
|
||||
|
||||
alias :after_invocation :append_after_invocation
|
||||
|
||||
def before_invocation_interceptors # :nodoc:
|
||||
read_inheritable_attribute("before_invocation_interceptors")
|
||||
end
|
||||
|
||||
def after_invocation_interceptors # :nodoc:
|
||||
read_inheritable_attribute("after_invocation_interceptors")
|
||||
end
|
||||
|
||||
def included_intercepted_methods # :nodoc:
|
||||
read_inheritable_attribute("included_intercepted_methods") || {}
|
||||
end
|
||||
|
||||
def excluded_intercepted_methods # :nodoc:
|
||||
read_inheritable_attribute("excluded_intercepted_methods") || {}
|
||||
end
|
||||
|
||||
private
|
||||
def append_interceptors_to_chain(condition, interceptors)
|
||||
write_inheritable_array("#{condition}_invocation_interceptors", interceptors)
|
||||
end
|
||||
|
||||
def prepend_interceptors_to_chain(condition, interceptors)
|
||||
interceptors = interceptors + read_inheritable_attribute("#{condition}_invocation_interceptors")
|
||||
write_inheritable_attribute("#{condition}_invocation_interceptors", interceptors)
|
||||
end
|
||||
|
||||
def extract_conditions!(interceptors)
|
||||
return nil unless interceptors.last.is_a? Hash
|
||||
interceptors.pop
|
||||
end
|
||||
|
||||
def add_interception_conditions(interceptors, conditions)
|
||||
return unless conditions
|
||||
included, excluded = conditions[:only], conditions[:except]
|
||||
write_inheritable_hash("included_intercepted_methods", condition_hash(interceptors, included)) && return if included
|
||||
write_inheritable_hash("excluded_intercepted_methods", condition_hash(interceptors, excluded)) if excluded
|
||||
end
|
||||
|
||||
def condition_hash(interceptors, *methods)
|
||||
interceptors.inject({}) {|hash, interceptor| hash.merge(interceptor => methods.flatten.map {|method| method.to_s})}
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
alias_method_chain :perform_invocation, :interception
|
||||
end
|
||||
end
|
||||
|
||||
def perform_invocation_with_interception(method_name, params, &block)
|
||||
return if before_invocation(method_name, params, &block) == false
|
||||
return_value = perform_invocation_without_interception(method_name, params)
|
||||
after_invocation(method_name, params, return_value)
|
||||
return_value
|
||||
end
|
||||
|
||||
def perform_invocation(method_name, params)
|
||||
send(method_name, *params)
|
||||
end
|
||||
|
||||
def before_invocation(name, args, &block)
|
||||
call_interceptors(self.class.before_invocation_interceptors, [name, args], &block)
|
||||
end
|
||||
|
||||
def after_invocation(name, args, result)
|
||||
call_interceptors(self.class.after_invocation_interceptors, [name, args, result])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def call_interceptors(interceptors, interceptor_args, &block)
|
||||
if interceptors and not interceptors.empty?
|
||||
interceptors.each do |interceptor|
|
||||
next if method_exempted?(interceptor, interceptor_args[0].to_s)
|
||||
result = case
|
||||
when interceptor.is_a?(Symbol)
|
||||
self.send(interceptor, *interceptor_args)
|
||||
when interceptor_block?(interceptor)
|
||||
interceptor.call(self, *interceptor_args)
|
||||
when interceptor_class?(interceptor)
|
||||
interceptor.intercept(self, *interceptor_args)
|
||||
else
|
||||
raise(
|
||||
InvocationError,
|
||||
"Interceptors need to be either a symbol, proc/method, or a class implementing a static intercept method"
|
||||
)
|
||||
end
|
||||
reason = nil
|
||||
if result.is_a?(Array)
|
||||
reason = result[1] if result[1]
|
||||
result = result[0]
|
||||
end
|
||||
if result == false
|
||||
block.call(reason) if block && reason
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def interceptor_block?(interceptor)
|
||||
interceptor.respond_to?("call") && (interceptor.arity == 3 || interceptor.arity == -1)
|
||||
end
|
||||
|
||||
def interceptor_class?(interceptor)
|
||||
interceptor.respond_to?("intercept")
|
||||
end
|
||||
|
||||
def method_exempted?(interceptor, method_name)
|
||||
case
|
||||
when self.class.included_intercepted_methods[interceptor]
|
||||
!self.class.included_intercepted_methods[interceptor].include?(method_name)
|
||||
when self.class.excluded_intercepted_methods[interceptor]
|
||||
self.class.excluded_intercepted_methods[interceptor].include?(method_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
require 'action_web_service/protocol/abstract'
|
||||
require 'action_web_service/protocol/discovery'
|
||||
require 'action_web_service/protocol/soap_protocol'
|
||||
require 'action_web_service/protocol/xmlrpc_protocol'
|
||||
@@ -1,112 +0,0 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Protocol # :nodoc:
|
||||
class ProtocolError < ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
class AbstractProtocol # :nodoc:
|
||||
def setup(controller)
|
||||
end
|
||||
|
||||
def decode_action_pack_request(action_pack_request)
|
||||
end
|
||||
|
||||
def encode_action_pack_request(service_name, public_method_name, raw_body, options={})
|
||||
klass = options[:request_class] || SimpleActionPackRequest
|
||||
request = klass.new
|
||||
request.request_parameters['action'] = service_name.to_s
|
||||
request.env['RAW_POST_DATA'] = raw_body
|
||||
request.env['REQUEST_METHOD'] = 'POST'
|
||||
request.env['HTTP_CONTENT_TYPE'] = 'text/xml'
|
||||
request
|
||||
end
|
||||
|
||||
def decode_request(raw_request, service_name, protocol_options={})
|
||||
end
|
||||
|
||||
def encode_request(method_name, params, param_types)
|
||||
end
|
||||
|
||||
def decode_response(raw_response)
|
||||
end
|
||||
|
||||
def encode_response(method_name, return_value, return_type, protocol_options={})
|
||||
end
|
||||
|
||||
def protocol_client(api, protocol_name, endpoint_uri, options)
|
||||
end
|
||||
|
||||
def register_api(api)
|
||||
end
|
||||
end
|
||||
|
||||
class Request # :nodoc:
|
||||
attr :protocol
|
||||
attr_accessor :method_name
|
||||
attr_accessor :method_params
|
||||
attr :service_name
|
||||
attr_accessor :api
|
||||
attr_accessor :api_method
|
||||
attr :protocol_options
|
||||
|
||||
def initialize(protocol, method_name, method_params, service_name, api=nil, api_method=nil, protocol_options=nil)
|
||||
@protocol = protocol
|
||||
@method_name = method_name
|
||||
@method_params = method_params
|
||||
@service_name = service_name
|
||||
@api = api
|
||||
@api_method = api_method
|
||||
@protocol_options = protocol_options || {}
|
||||
end
|
||||
end
|
||||
|
||||
class Response # :nodoc:
|
||||
attr :body
|
||||
attr :content_type
|
||||
attr :return_value
|
||||
|
||||
def initialize(body, content_type, return_value)
|
||||
@body = body
|
||||
@content_type = content_type
|
||||
@return_value = return_value
|
||||
end
|
||||
end
|
||||
|
||||
class SimpleActionPackRequest < ActionController::AbstractRequest # :nodoc:
|
||||
def initialize
|
||||
@env = {}
|
||||
@qparams = {}
|
||||
@rparams = {}
|
||||
@cookies = {}
|
||||
reset_session
|
||||
end
|
||||
|
||||
def query_parameters
|
||||
@qparams
|
||||
end
|
||||
|
||||
def request_parameters
|
||||
@rparams
|
||||
end
|
||||
|
||||
def env
|
||||
@env
|
||||
end
|
||||
|
||||
def host
|
||||
''
|
||||
end
|
||||
|
||||
def cookies
|
||||
@cookies
|
||||
end
|
||||
|
||||
def session
|
||||
@session
|
||||
end
|
||||
|
||||
def reset_session
|
||||
@session = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Protocol # :nodoc:
|
||||
module Discovery # :nodoc:
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
base.send(:include, ActionWebService::Protocol::Discovery::InstanceMethods)
|
||||
end
|
||||
|
||||
module ClassMethods # :nodoc:
|
||||
def register_protocol(klass)
|
||||
write_inheritable_array("web_service_protocols", [klass])
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
private
|
||||
def discover_web_service_request(action_pack_request)
|
||||
(self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol|
|
||||
protocol = protocol.create(self)
|
||||
request = protocol.decode_action_pack_request(action_pack_request)
|
||||
return request unless request.nil?
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def create_web_service_client(api, protocol_name, endpoint_uri, options)
|
||||
(self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol|
|
||||
protocol = protocol.create(self)
|
||||
client = protocol.protocol_client(api, protocol_name, endpoint_uri, options)
|
||||
return client unless client.nil?
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-176
@@ -1,176 +0,0 @@
|
||||
require 'action_web_service/protocol/soap_protocol/marshaler'
|
||||
require 'soap/streamHandler'
|
||||
require 'action_web_service/client/soap_client'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module API # :nodoc:
|
||||
class Base # :nodoc:
|
||||
def self.soap_client(endpoint_uri, options={})
|
||||
ActionWebService::Client::Soap.new self, endpoint_uri, options
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Protocol # :nodoc:
|
||||
module Soap # :nodoc:
|
||||
def self.included(base)
|
||||
base.register_protocol(SoapProtocol)
|
||||
base.class_inheritable_option(:wsdl_service_name)
|
||||
base.class_inheritable_option(:wsdl_namespace)
|
||||
end
|
||||
|
||||
class SoapProtocol < AbstractProtocol # :nodoc:
|
||||
AWSEncoding = 'UTF-8'
|
||||
XSDEncoding = 'UTF8'
|
||||
|
||||
attr :marshaler
|
||||
|
||||
def initialize(namespace=nil)
|
||||
namespace ||= 'urn:ActionWebService'
|
||||
@marshaler = SoapMarshaler.new namespace
|
||||
end
|
||||
|
||||
def self.create(controller)
|
||||
SoapProtocol.new(controller.wsdl_namespace)
|
||||
end
|
||||
|
||||
def decode_action_pack_request(action_pack_request)
|
||||
return nil unless soap_action = has_valid_soap_action?(action_pack_request)
|
||||
service_name = action_pack_request.parameters['action']
|
||||
input_encoding = parse_charset(action_pack_request.env['HTTP_CONTENT_TYPE'])
|
||||
protocol_options = {
|
||||
:soap_action => soap_action,
|
||||
:charset => input_encoding
|
||||
}
|
||||
decode_request(action_pack_request.raw_post, service_name, protocol_options)
|
||||
end
|
||||
|
||||
def encode_action_pack_request(service_name, public_method_name, raw_body, options={})
|
||||
request = super
|
||||
request.env['HTTP_SOAPACTION'] = '/soap/%s/%s' % [service_name, public_method_name]
|
||||
request
|
||||
end
|
||||
|
||||
def decode_request(raw_request, service_name, protocol_options={})
|
||||
envelope = SOAP::Processor.unmarshal(raw_request, :charset => protocol_options[:charset])
|
||||
unless envelope
|
||||
raise ProtocolError, "Failed to parse SOAP request message"
|
||||
end
|
||||
request = envelope.body.request
|
||||
method_name = request.elename.name
|
||||
params = request.collect{ |k, v| marshaler.soap_to_ruby(request[k]) }
|
||||
Request.new(self, method_name, params, service_name, nil, nil, protocol_options)
|
||||
end
|
||||
|
||||
def encode_request(method_name, params, param_types)
|
||||
param_types.each{ |type| marshaler.register_type(type) } if param_types
|
||||
qname = XSD::QName.new(marshaler.namespace, method_name)
|
||||
param_def = []
|
||||
if param_types
|
||||
params = param_types.zip(params).map do |type, param|
|
||||
param_def << ['in', type.name, marshaler.lookup_type(type).mapping]
|
||||
[type.name, marshaler.ruby_to_soap(param)]
|
||||
end
|
||||
else
|
||||
params = []
|
||||
end
|
||||
request = SOAP::RPC::SOAPMethodRequest.new(qname, param_def)
|
||||
request.set_param(params)
|
||||
envelope = create_soap_envelope(request)
|
||||
SOAP::Processor.marshal(envelope)
|
||||
end
|
||||
|
||||
def decode_response(raw_response)
|
||||
envelope = SOAP::Processor.unmarshal(raw_response)
|
||||
unless envelope
|
||||
raise ProtocolError, "Failed to parse SOAP request message"
|
||||
end
|
||||
method_name = envelope.body.request.elename.name
|
||||
return_value = envelope.body.response
|
||||
return_value = marshaler.soap_to_ruby(return_value) unless return_value.nil?
|
||||
[method_name, return_value]
|
||||
end
|
||||
|
||||
def encode_response(method_name, return_value, return_type, protocol_options={})
|
||||
if return_type
|
||||
return_binding = marshaler.register_type(return_type)
|
||||
marshaler.annotate_arrays(return_binding, return_value)
|
||||
end
|
||||
qname = XSD::QName.new(marshaler.namespace, method_name)
|
||||
if return_value.nil?
|
||||
response = SOAP::RPC::SOAPMethodResponse.new(qname, nil)
|
||||
else
|
||||
if return_value.is_a?(Exception)
|
||||
detail = SOAP::Mapping::SOAPException.new(return_value)
|
||||
response = SOAP::SOAPFault.new(
|
||||
SOAP::SOAPQName.new('%s:%s' % [SOAP::SOAPNamespaceTag, 'Server']),
|
||||
SOAP::SOAPString.new(return_value.to_s),
|
||||
SOAP::SOAPString.new(self.class.name),
|
||||
marshaler.ruby_to_soap(detail))
|
||||
else
|
||||
if return_type
|
||||
param_def = [['retval', 'return', marshaler.lookup_type(return_type).mapping]]
|
||||
response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def)
|
||||
response.retval = marshaler.ruby_to_soap(return_value)
|
||||
else
|
||||
response = SOAP::RPC::SOAPMethodResponse.new(qname, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
envelope = create_soap_envelope(response)
|
||||
|
||||
# FIXME: This is not thread-safe, but StringFactory_ in SOAP4R only
|
||||
# reads target encoding from the XSD::Charset.encoding variable.
|
||||
# This is required to ensure $KCODE strings are converted
|
||||
# correctly to UTF-8 for any values of $KCODE.
|
||||
previous_encoding = XSD::Charset.encoding
|
||||
XSD::Charset.encoding = XSDEncoding
|
||||
response_body = SOAP::Processor.marshal(envelope, :charset => AWSEncoding)
|
||||
XSD::Charset.encoding = previous_encoding
|
||||
|
||||
Response.new(response_body, "text/xml; charset=#{AWSEncoding}", return_value)
|
||||
end
|
||||
|
||||
def protocol_client(api, protocol_name, endpoint_uri, options={})
|
||||
return nil unless protocol_name == :soap
|
||||
ActionWebService::Client::Soap.new(api, endpoint_uri, options)
|
||||
end
|
||||
|
||||
def register_api(api)
|
||||
api.api_methods.each do |name, method|
|
||||
method.expects.each{ |type| marshaler.register_type(type) } if method.expects
|
||||
method.returns.each{ |type| marshaler.register_type(type) } if method.returns
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def has_valid_soap_action?(request)
|
||||
return nil unless request.method == :post
|
||||
soap_action = request.env['HTTP_SOAPACTION']
|
||||
return nil unless soap_action
|
||||
soap_action = soap_action.dup
|
||||
soap_action.gsub!(/^"/, '')
|
||||
soap_action.gsub!(/"$/, '')
|
||||
soap_action.strip!
|
||||
return nil if soap_action.empty?
|
||||
soap_action
|
||||
end
|
||||
|
||||
def create_soap_envelope(body)
|
||||
header = SOAP::SOAPHeader.new
|
||||
body = SOAP::SOAPBody.new(body)
|
||||
SOAP::SOAPEnvelope.new(header, body)
|
||||
end
|
||||
|
||||
def parse_charset(content_type)
|
||||
return AWSEncoding if content_type.nil?
|
||||
if /^text\/xml(?:\s*;\s*charset=([^"]+|"[^"]+"))$/i =~ content_type
|
||||
$1
|
||||
else
|
||||
AWSEncoding
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-235
@@ -1,235 +0,0 @@
|
||||
require 'soap/mapping'
|
||||
|
||||
module ActionWebService
|
||||
module Protocol
|
||||
module Soap
|
||||
# Workaround for SOAP4R return values changing
|
||||
class Registry < SOAP::Mapping::Registry
|
||||
if SOAP::Version >= "1.5.4"
|
||||
def find_mapped_soap_class(obj_class)
|
||||
return @map.instance_eval { @obj2soap[obj_class][0] }
|
||||
end
|
||||
|
||||
def find_mapped_obj_class(soap_class)
|
||||
return @map.instance_eval { @soap2obj[soap_class][0] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class SoapMarshaler
|
||||
attr :namespace
|
||||
attr :registry
|
||||
|
||||
def initialize(namespace=nil)
|
||||
@namespace = namespace || 'urn:ActionWebService'
|
||||
@registry = Registry.new
|
||||
@type2binding = {}
|
||||
register_static_factories
|
||||
end
|
||||
|
||||
def soap_to_ruby(obj)
|
||||
SOAP::Mapping.soap2obj(obj, @registry)
|
||||
end
|
||||
|
||||
def ruby_to_soap(obj)
|
||||
soap = SOAP::Mapping.obj2soap(obj, @registry)
|
||||
soap.elename = XSD::QName.new if SOAP::Version >= "1.5.5" && soap.elename == XSD::QName::EMPTY
|
||||
soap
|
||||
end
|
||||
|
||||
def register_type(type)
|
||||
return @type2binding[type] if @type2binding.has_key?(type)
|
||||
|
||||
if type.array?
|
||||
array_mapping = @registry.find_mapped_soap_class(Array)
|
||||
qname = XSD::QName.new(@namespace, soap_type_name(type.element_type.type_class.name) + 'Array')
|
||||
element_type_binding = register_type(type.element_type)
|
||||
@type2binding[type] = SoapBinding.new(self, qname, type, array_mapping, element_type_binding)
|
||||
elsif (mapping = @registry.find_mapped_soap_class(type.type_class) rescue nil)
|
||||
qname = mapping[2] ? mapping[2][:type] : nil
|
||||
qname ||= soap_base_type_name(mapping[0])
|
||||
@type2binding[type] = SoapBinding.new(self, qname, type, mapping)
|
||||
else
|
||||
qname = XSD::QName.new(@namespace, soap_type_name(type.type_class.name))
|
||||
@registry.add(type.type_class,
|
||||
SOAP::SOAPStruct,
|
||||
typed_struct_factory(type.type_class),
|
||||
{ :type => qname })
|
||||
mapping = @registry.find_mapped_soap_class(type.type_class)
|
||||
@type2binding[type] = SoapBinding.new(self, qname, type, mapping)
|
||||
end
|
||||
|
||||
if type.structured?
|
||||
type.each_member do |m_name, m_type|
|
||||
register_type(m_type)
|
||||
end
|
||||
end
|
||||
|
||||
@type2binding[type]
|
||||
end
|
||||
alias :lookup_type :register_type
|
||||
|
||||
def annotate_arrays(binding, value)
|
||||
if value.nil?
|
||||
return
|
||||
elsif binding.type.array?
|
||||
mark_typed_array(value, binding.element_binding.qname)
|
||||
if binding.element_binding.type.custom?
|
||||
value.each do |element|
|
||||
annotate_arrays(binding.element_binding, element)
|
||||
end
|
||||
end
|
||||
elsif binding.type.structured?
|
||||
binding.type.each_member do |name, type|
|
||||
member_binding = register_type(type)
|
||||
member_value = value.respond_to?('[]') ? value[name] : value.send(name)
|
||||
annotate_arrays(member_binding, member_value) if type.custom?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def typed_struct_factory(type_class)
|
||||
if Object.const_defined?('ActiveRecord')
|
||||
if type_class.ancestors.include?(ActiveRecord::Base)
|
||||
qname = XSD::QName.new(@namespace, soap_type_name(type_class.name))
|
||||
type_class.instance_variable_set('@qname', qname)
|
||||
return SoapActiveRecordStructFactory.new
|
||||
end
|
||||
end
|
||||
SOAP::Mapping::Registry::TypedStructFactory
|
||||
end
|
||||
|
||||
def mark_typed_array(array, qname)
|
||||
(class << array; self; end).class_eval do
|
||||
define_method(:arytype) do
|
||||
qname
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def soap_base_type_name(type)
|
||||
xsd_type = type.ancestors.find{ |c| c.const_defined? 'Type' }
|
||||
xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type
|
||||
end
|
||||
|
||||
def soap_type_name(type_name)
|
||||
type_name.gsub(/::/, '..')
|
||||
end
|
||||
|
||||
def register_static_factories
|
||||
@registry.add(ActionWebService::Base64, SOAP::SOAPBase64, SoapBase64Factory.new, nil)
|
||||
mapping = @registry.find_mapped_soap_class(ActionWebService::Base64)
|
||||
@type2binding[ActionWebService::Base64] =
|
||||
SoapBinding.new(self, SOAP::SOAPBase64::Type, ActionWebService::Base64, mapping)
|
||||
@registry.add(Array, SOAP::SOAPArray, SoapTypedArrayFactory.new, nil)
|
||||
@registry.add(::BigDecimal, SOAP::SOAPDouble, SOAP::Mapping::Registry::BasetypeFactory, {:derived_class => true})
|
||||
end
|
||||
end
|
||||
|
||||
class SoapBinding
|
||||
attr :qname
|
||||
attr :type
|
||||
attr :mapping
|
||||
attr :element_binding
|
||||
|
||||
def initialize(marshaler, qname, type, mapping, element_binding=nil)
|
||||
@marshaler = marshaler
|
||||
@qname = qname
|
||||
@type = type
|
||||
@mapping = mapping
|
||||
@element_binding = element_binding
|
||||
end
|
||||
|
||||
def type_name
|
||||
@type.custom? ? @qname.name : nil
|
||||
end
|
||||
|
||||
def qualified_type_name(ns=nil)
|
||||
if @type.custom?
|
||||
"#{ns ? ns : @qname.namespace}:#{@qname.name}"
|
||||
else
|
||||
ns = XSD::NS.new
|
||||
ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag)
|
||||
ns.assign(SOAP::EncodingNamespace, "soapenc")
|
||||
xsd_klass = mapping[0].ancestors.find{|c| c.const_defined?('Type')}
|
||||
return ns.name(XSD::AnyTypeName) unless xsd_klass
|
||||
ns.name(xsd_klass.const_get('Type'))
|
||||
end
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
@qname == other.qname
|
||||
end
|
||||
alias :== :eql?
|
||||
|
||||
def hash
|
||||
@qname.hash
|
||||
end
|
||||
end
|
||||
|
||||
class SoapActiveRecordStructFactory < SOAP::Mapping::Factory
|
||||
def obj2soap(soap_class, obj, info, map)
|
||||
unless obj.is_a?(ActiveRecord::Base)
|
||||
return nil
|
||||
end
|
||||
soap_obj = soap_class.new(obj.class.instance_variable_get('@qname'))
|
||||
obj.class.columns.each do |column|
|
||||
key = column.name.to_s
|
||||
value = obj.send(key)
|
||||
soap_obj[key] = SOAP::Mapping._obj2soap(value, map)
|
||||
end
|
||||
soap_obj
|
||||
end
|
||||
|
||||
def soap2obj(obj_class, node, info, map)
|
||||
unless node.type == obj_class.instance_variable_get('@qname')
|
||||
return false
|
||||
end
|
||||
obj = obj_class.new
|
||||
node.each do |key, value|
|
||||
obj[key] = value.data
|
||||
end
|
||||
obj.instance_variable_set('@new_record', false)
|
||||
return true, obj
|
||||
end
|
||||
end
|
||||
|
||||
class SoapTypedArrayFactory < SOAP::Mapping::Factory
|
||||
def obj2soap(soap_class, obj, info, map)
|
||||
unless obj.respond_to?(:arytype)
|
||||
return nil
|
||||
end
|
||||
soap_obj = soap_class.new(SOAP::ValueArrayName, 1, obj.arytype)
|
||||
mark_marshalled_obj(obj, soap_obj)
|
||||
obj.each do |item|
|
||||
child = SOAP::Mapping._obj2soap(item, map)
|
||||
soap_obj.add(child)
|
||||
end
|
||||
soap_obj
|
||||
end
|
||||
|
||||
def soap2obj(obj_class, node, info, map)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
class SoapBase64Factory < SOAP::Mapping::Factory
|
||||
def obj2soap(soap_class, obj, info, map)
|
||||
unless obj.is_a?(ActionWebService::Base64)
|
||||
return nil
|
||||
end
|
||||
return soap_class.new(obj)
|
||||
end
|
||||
|
||||
def soap2obj(obj_class, node, info, map)
|
||||
unless node.type == SOAP::SOAPBase64::Type
|
||||
return false
|
||||
end
|
||||
return true, obj_class.new(node.string)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
require 'xmlrpc/marshal'
|
||||
require 'action_web_service/client/xmlrpc_client'
|
||||
|
||||
module XMLRPC # :nodoc:
|
||||
class FaultException # :nodoc:
|
||||
alias :message :faultString
|
||||
end
|
||||
|
||||
class Create
|
||||
def wrong_type(value)
|
||||
if BigDecimal === value
|
||||
[true, value.to_f]
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module API # :nodoc:
|
||||
class Base # :nodoc:
|
||||
def self.xmlrpc_client(endpoint_uri, options={})
|
||||
ActionWebService::Client::XmlRpc.new self, endpoint_uri, options
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Protocol # :nodoc:
|
||||
module XmlRpc # :nodoc:
|
||||
def self.included(base)
|
||||
base.register_protocol(XmlRpcProtocol)
|
||||
end
|
||||
|
||||
class XmlRpcProtocol < AbstractProtocol # :nodoc:
|
||||
def self.create(controller)
|
||||
XmlRpcProtocol.new
|
||||
end
|
||||
|
||||
def decode_action_pack_request(action_pack_request)
|
||||
service_name = action_pack_request.parameters['action']
|
||||
decode_request(action_pack_request.raw_post, service_name)
|
||||
end
|
||||
|
||||
def decode_request(raw_request, service_name)
|
||||
method_name, params = XMLRPC::Marshal.load_call(raw_request)
|
||||
Request.new(self, method_name, params, service_name)
|
||||
rescue
|
||||
return nil
|
||||
end
|
||||
|
||||
def encode_request(method_name, params, param_types)
|
||||
if param_types
|
||||
params = params.dup
|
||||
param_types.each_with_index{ |type, i| params[i] = value_to_xmlrpc_wire_format(params[i], type) }
|
||||
end
|
||||
XMLRPC::Marshal.dump_call(method_name, *params)
|
||||
end
|
||||
|
||||
def decode_response(raw_response)
|
||||
[nil, XMLRPC::Marshal.load_response(raw_response)]
|
||||
end
|
||||
|
||||
def encode_response(method_name, return_value, return_type, protocol_options={})
|
||||
if return_value && return_type
|
||||
return_value = value_to_xmlrpc_wire_format(return_value, return_type)
|
||||
end
|
||||
return_value = false if return_value.nil?
|
||||
raw_response = XMLRPC::Marshal.dump_response(return_value)
|
||||
Response.new(raw_response, 'text/xml', return_value)
|
||||
end
|
||||
|
||||
def encode_multicall_response(responses, protocol_options={})
|
||||
result = responses.map do |return_value, return_type|
|
||||
if return_value && return_type
|
||||
return_value = value_to_xmlrpc_wire_format(return_value, return_type)
|
||||
return_value = [return_value] unless return_value.nil?
|
||||
end
|
||||
return_value = false if return_value.nil?
|
||||
return_value
|
||||
end
|
||||
raw_response = XMLRPC::Marshal.dump_response(result)
|
||||
Response.new(raw_response, 'text/xml', result)
|
||||
end
|
||||
|
||||
def protocol_client(api, protocol_name, endpoint_uri, options={})
|
||||
return nil unless protocol_name == :xmlrpc
|
||||
ActionWebService::Client::XmlRpc.new(api, endpoint_uri, options)
|
||||
end
|
||||
|
||||
def value_to_xmlrpc_wire_format(value, value_type)
|
||||
if value_type.array?
|
||||
value.map{ |val| value_to_xmlrpc_wire_format(val, value_type.element_type) }
|
||||
else
|
||||
if value.is_a?(ActionWebService::Struct)
|
||||
struct = {}
|
||||
value.class.members.each do |name, type|
|
||||
member_value = value[name]
|
||||
next if member_value.nil?
|
||||
struct[name.to_s] = value_to_xmlrpc_wire_format(member_value, type)
|
||||
end
|
||||
struct
|
||||
elsif value.is_a?(ActiveRecord::Base)
|
||||
struct = {}
|
||||
value.attributes.each do |key, member_value|
|
||||
next if member_value.nil?
|
||||
struct[key.to_s] = member_value
|
||||
end
|
||||
struct
|
||||
elsif value.is_a?(ActionWebService::Base64)
|
||||
XMLRPC::Base64.new(value)
|
||||
elsif value.is_a?(Exception) && !value.is_a?(XMLRPC::FaultException)
|
||||
XMLRPC::FaultException.new(2, value.message)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,283 +0,0 @@
|
||||
require 'benchmark'
|
||||
require 'pathname'
|
||||
|
||||
module ActionWebService
|
||||
module Scaffolding # :nodoc:
|
||||
class ScaffoldingError < ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Web service invocation scaffolding provides a way to quickly invoke web service methods in a controller. The
|
||||
# generated scaffold actions have default views to let you enter the method parameters and view the
|
||||
# results.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class ApiController < ActionController
|
||||
# web_service_scaffold :invoke
|
||||
# end
|
||||
#
|
||||
# This example generates an +invoke+ action in the +ApiController+ that you can navigate to from
|
||||
# your browser, select the API method, enter its parameters, and perform the invocation.
|
||||
#
|
||||
# If you want to customize the default views, create the following views in "app/views":
|
||||
#
|
||||
# * <tt>action_name/methods.erb</tt>
|
||||
# * <tt>action_name/parameters.erb</tt>
|
||||
# * <tt>action_name/result.erb</tt>
|
||||
# * <tt>action_name/layout.erb</tt>
|
||||
#
|
||||
# Where <tt>action_name</tt> is the name of the action you gave to ClassMethods#web_service_scaffold.
|
||||
#
|
||||
# You can use the default views in <tt>RAILS_DIR/lib/action_web_service/templates/scaffolds</tt> as
|
||||
# a guide.
|
||||
module ClassMethods
|
||||
# Generates web service invocation scaffolding for the current controller. The given action name
|
||||
# can then be used as the entry point for invoking API methods from a web browser.
|
||||
def web_service_scaffold(action_name)
|
||||
add_template_helper(Helpers)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__ + 1
|
||||
def #{action_name}
|
||||
if request.method == :get
|
||||
setup_invocation_assigns
|
||||
render_invocation_scaffold 'methods'
|
||||
end
|
||||
end
|
||||
|
||||
def #{action_name}_method_params
|
||||
if request.method == :get
|
||||
setup_invocation_assigns
|
||||
render_invocation_scaffold 'parameters'
|
||||
end
|
||||
end
|
||||
|
||||
def #{action_name}_submit
|
||||
if request.method == :post
|
||||
setup_invocation_assigns
|
||||
protocol_name = params['protocol'] ? params['protocol'].to_sym : :soap
|
||||
case protocol_name
|
||||
when :soap
|
||||
@protocol = Protocol::Soap::SoapProtocol.create(self)
|
||||
when :xmlrpc
|
||||
@protocol = Protocol::XmlRpc::XmlRpcProtocol.create(self)
|
||||
end
|
||||
bm = Benchmark.measure do
|
||||
@protocol.register_api(@scaffold_service.api)
|
||||
post_params = params['method_params'] ? params['method_params'].dup : nil
|
||||
params = []
|
||||
@scaffold_method.expects.each_with_index do |spec, i|
|
||||
params << post_params[i.to_s]
|
||||
end if @scaffold_method.expects
|
||||
params = @scaffold_method.cast_expects(params)
|
||||
method_name = public_method_name(@scaffold_service.name, @scaffold_method.public_name)
|
||||
@method_request_xml = @protocol.encode_request(method_name, params, @scaffold_method.expects)
|
||||
new_request = @protocol.encode_action_pack_request(@scaffold_service.name, @scaffold_method.public_name, @method_request_xml)
|
||||
prepare_request(new_request, @scaffold_service.name, @scaffold_method.public_name)
|
||||
self.request = new_request
|
||||
if @scaffold_container.dispatching_mode != :direct
|
||||
request.parameters['action'] = @scaffold_service.name
|
||||
end
|
||||
dispatch_web_service_request
|
||||
@method_response_xml = response.body
|
||||
method_name, obj = @protocol.decode_response(@method_response_xml)
|
||||
return if handle_invocation_exception(obj)
|
||||
@method_return_value = @scaffold_method.cast_returns(obj)
|
||||
end
|
||||
@method_elapsed = bm.real
|
||||
add_instance_variables_to_assigns
|
||||
reset_invocation_response
|
||||
render_invocation_scaffold 'result'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def setup_invocation_assigns
|
||||
@scaffold_class = self.class
|
||||
@scaffold_action_name = "#{action_name}"
|
||||
@scaffold_container = WebServiceModel::Container.new(self)
|
||||
if params['service'] && params['method']
|
||||
@scaffold_service = @scaffold_container.services.find{ |x| x.name == params['service'] }
|
||||
@scaffold_method = @scaffold_service.api_methods[params['method']]
|
||||
end
|
||||
add_instance_variables_to_assigns
|
||||
end
|
||||
|
||||
def render_invocation_scaffold(action)
|
||||
customized_template = "\#{self.class.controller_path}/#{action_name}/\#{action}"
|
||||
default_template = scaffold_path(action)
|
||||
if template_exists?(customized_template)
|
||||
content = @template.render :file => customized_template
|
||||
else
|
||||
content = @template.render :file => default_template
|
||||
end
|
||||
@template.instance_variable_set("@content_for_layout", content)
|
||||
if self.active_layout.nil?
|
||||
render :file => scaffold_path("layout")
|
||||
else
|
||||
render :file => self.active_layout
|
||||
end
|
||||
end
|
||||
|
||||
def scaffold_path(template_name)
|
||||
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".erb"
|
||||
end
|
||||
|
||||
def reset_invocation_response
|
||||
erase_render_results
|
||||
response.headers = ::ActionController::AbstractResponse::DEFAULT_HEADERS.merge("cookie" => [])
|
||||
end
|
||||
|
||||
def public_method_name(service_name, method_name)
|
||||
if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol)
|
||||
service_name + '.' + method_name
|
||||
else
|
||||
method_name
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_request(new_request, service_name, method_name)
|
||||
new_request.parameters.update(request.parameters)
|
||||
request.env.each{ |k, v| new_request.env[k] = v unless new_request.env.has_key?(k) }
|
||||
if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::Soap::SoapProtocol)
|
||||
new_request.env['HTTP_SOAPACTION'] = "/\#{controller_name()}/\#{service_name}/\#{method_name}"
|
||||
end
|
||||
end
|
||||
|
||||
def handle_invocation_exception(obj)
|
||||
exception = nil
|
||||
if obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && obj.detail.cause.is_a?(Exception)
|
||||
exception = obj.detail.cause
|
||||
elsif obj.is_a?(XMLRPC::FaultException)
|
||||
exception = obj
|
||||
end
|
||||
return unless exception
|
||||
reset_invocation_response
|
||||
rescue_action(exception)
|
||||
true
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
end
|
||||
|
||||
module Helpers # :nodoc:
|
||||
def method_parameter_input_fields(method, type, field_name_base, idx, was_structured=false)
|
||||
if type.array?
|
||||
return content_tag('em', "Typed array input fields not supported yet (#{type.name})")
|
||||
end
|
||||
if type.structured?
|
||||
return content_tag('em', "Nested structural types not supported yet (#{type.name})") if was_structured
|
||||
parameters = ""
|
||||
type.each_member do |member_name, member_type|
|
||||
label = method_parameter_label(member_name, member_type)
|
||||
nested_content = method_parameter_input_fields(
|
||||
method,
|
||||
member_type,
|
||||
"#{field_name_base}[#{idx}][#{member_name}]",
|
||||
idx,
|
||||
true)
|
||||
if member_type.custom?
|
||||
parameters << content_tag('li', label)
|
||||
parameters << content_tag('ul', nested_content)
|
||||
else
|
||||
parameters << content_tag('li', label + ' ' + nested_content)
|
||||
end
|
||||
end
|
||||
content_tag('ul', parameters)
|
||||
else
|
||||
# If the data source was structured previously we already have the index set
|
||||
field_name_base = "#{field_name_base}[#{idx}]" unless was_structured
|
||||
|
||||
case type.type
|
||||
when :int
|
||||
text_field_tag "#{field_name_base}"
|
||||
when :string
|
||||
text_field_tag "#{field_name_base}"
|
||||
when :base64
|
||||
text_area_tag "#{field_name_base}", nil, :size => "40x5"
|
||||
when :bool
|
||||
radio_button_tag("#{field_name_base}", "true") + " True" +
|
||||
radio_button_tag("#{field_name_base}", "false") + "False"
|
||||
when :float
|
||||
text_field_tag "#{field_name_base}"
|
||||
when :time, :datetime
|
||||
time = Time.now
|
||||
i = 0
|
||||
%w|year month day hour minute second|.map do |name|
|
||||
i += 1
|
||||
send("select_#{name}", time, :prefix => "#{field_name_base}[#{i}]", :discard_type => true)
|
||||
end.join
|
||||
when :date
|
||||
date = Date.today
|
||||
i = 0
|
||||
%w|year month day|.map do |name|
|
||||
i += 1
|
||||
send("select_#{name}", date, :prefix => "#{field_name_base}[#{i}]", :discard_type => true)
|
||||
end.join
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def method_parameter_label(name, type)
|
||||
name.to_s.capitalize + ' (' + type.human_name(false) + ')'
|
||||
end
|
||||
|
||||
def service_method_list(service)
|
||||
action = @scaffold_action_name + '_method_params'
|
||||
methods = service.api_methods_full.map do |desc, name|
|
||||
content_tag("li", link_to(desc, :action => action, :service => service.name, :method => name))
|
||||
end
|
||||
content_tag("ul", methods.join("\n"))
|
||||
end
|
||||
end
|
||||
|
||||
module WebServiceModel # :nodoc:
|
||||
class Container # :nodoc:
|
||||
attr :services
|
||||
attr :dispatching_mode
|
||||
|
||||
def initialize(real_container)
|
||||
@real_container = real_container
|
||||
@dispatching_mode = @real_container.class.web_service_dispatching_mode
|
||||
@services = []
|
||||
if @dispatching_mode == :direct
|
||||
@services << Service.new(@real_container.controller_name, @real_container)
|
||||
else
|
||||
@real_container.class.web_services.each do |name, obj|
|
||||
@services << Service.new(name, @real_container.instance_eval{ web_service_object(name) })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Service # :nodoc:
|
||||
attr :name
|
||||
attr :object
|
||||
attr :api
|
||||
attr :api_methods
|
||||
attr :api_methods_full
|
||||
|
||||
def initialize(name, real_service)
|
||||
@name = name.to_s
|
||||
@object = real_service
|
||||
@api = @object.class.web_service_api
|
||||
if @api.nil?
|
||||
raise ScaffoldingError, "No web service API attached to #{object.class}"
|
||||
end
|
||||
@api_methods = {}
|
||||
@api_methods_full = []
|
||||
@api.api_methods.each do |name, method|
|
||||
@api_methods[method.public_name.to_s] = method
|
||||
@api_methods_full << [method.to_s, method.public_name.to_s]
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
self.name.camelize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,64 +0,0 @@
|
||||
module ActionWebService
|
||||
# To send structured types across the wire, derive from ActionWebService::Struct,
|
||||
# and use +member+ to declare structure members.
|
||||
#
|
||||
# ActionWebService::Struct should be used in method signatures when you want to accept or return
|
||||
# structured types that have no Active Record model class representations, or you don't
|
||||
# want to expose your entire Active Record model to remote callers.
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# class Person < ActionWebService::Struct
|
||||
# member :id, :int
|
||||
# member :firstnames, [:string]
|
||||
# member :lastname, :string
|
||||
# member :email, :string
|
||||
# end
|
||||
# person = Person.new(:id => 5, :firstname => 'john', :lastname => 'doe')
|
||||
#
|
||||
# Active Record model classes are already implicitly supported in method
|
||||
# signatures.
|
||||
class Struct
|
||||
# If a Hash is given as argument to an ActionWebService::Struct constructor,
|
||||
# it can contain initial values for the structure member.
|
||||
def initialize(values={})
|
||||
if values.is_a?(Hash)
|
||||
values.map{|k,v| __send__('%s=' % k.to_s, v)}
|
||||
end
|
||||
end
|
||||
|
||||
# The member with the given name
|
||||
def [](name)
|
||||
send(name.to_s)
|
||||
end
|
||||
|
||||
# Iterates through each member
|
||||
def each_pair(&block)
|
||||
self.class.members.each do |name, type|
|
||||
yield name, self.__send__(name)
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
# Creates a structure member with the specified +name+ and +type+. Generates
|
||||
# accessor methods for reading and writing the member value.
|
||||
def member(name, type)
|
||||
name = name.to_sym
|
||||
type = ActionWebService::SignatureTypes.canonical_signature_entry({ name => type }, 0)
|
||||
write_inheritable_hash("struct_members", name => type)
|
||||
class_eval <<-END
|
||||
def #{name}; @#{name}; end
|
||||
def #{name}=(value); @#{name} = value; end
|
||||
END
|
||||
end
|
||||
|
||||
def members # :nodoc:
|
||||
read_inheritable_attribute("struct_members") || {}
|
||||
end
|
||||
|
||||
def member_type(name) # :nodoc:
|
||||
members[name.to_sym]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Vendored
-26
@@ -1,26 +0,0 @@
|
||||
class Class # :nodoc:
|
||||
def class_inheritable_option(sym, default_value=nil)
|
||||
write_inheritable_attribute sym, default_value
|
||||
class_eval <<-EOS
|
||||
def self.#{sym}(value=nil)
|
||||
if !value.nil?
|
||||
write_inheritable_attribute(:#{sym}, value)
|
||||
else
|
||||
read_inheritable_attribute(:#{sym})
|
||||
end
|
||||
end
|
||||
|
||||
def self.#{sym}=(value)
|
||||
write_inheritable_attribute(:#{sym}, value)
|
||||
end
|
||||
|
||||
def #{sym}
|
||||
self.class.#{sym}
|
||||
end
|
||||
|
||||
def #{sym}=(value)
|
||||
self.class.#{sym} = value
|
||||
end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
-226
@@ -1,226 +0,0 @@
|
||||
module ActionWebService # :nodoc:
|
||||
# Action Web Service supports the following base types in a signature:
|
||||
#
|
||||
# [<tt>:int</tt>] Represents an integer value, will be cast to an integer using <tt>Integer(value)</tt>
|
||||
# [<tt>:string</tt>] Represents a string value, will be cast to an string using the <tt>to_s</tt> method on an object
|
||||
# [<tt>:base64</tt>] Represents a Base 64 value, will contain the binary bytes of a Base 64 value sent by the caller
|
||||
# [<tt>:bool</tt>] Represents a boolean value, whatever is passed will be cast to boolean (<tt>true</tt>, '1', 'true', 'y', 'yes' are taken to represent true; <tt>false</tt>, '0', 'false', 'n', 'no' and <tt>nil</tt> represent false)
|
||||
# [<tt>:float</tt>] Represents a floating point value, will be cast to a float using <tt>Float(value)</tt>
|
||||
# [<tt>:time</tt>] Represents a timestamp, will be cast to a <tt>Time</tt> object
|
||||
# [<tt>:datetime</tt>] Represents a timestamp, will be cast to a <tt>DateTime</tt> object
|
||||
# [<tt>:date</tt>] Represents a date, will be cast to a <tt>Date</tt> object
|
||||
#
|
||||
# For structured types, you'll need to pass in the Class objects of
|
||||
# ActionWebService::Struct and ActiveRecord::Base derivatives.
|
||||
module SignatureTypes
|
||||
def canonical_signature(signature) # :nodoc:
|
||||
return nil if signature.nil?
|
||||
unless signature.is_a?(Array)
|
||||
raise(ActionWebServiceError, "Expected signature to be an Array")
|
||||
end
|
||||
i = -1
|
||||
signature.map{ |spec| canonical_signature_entry(spec, i += 1) }
|
||||
end
|
||||
|
||||
def canonical_signature_entry(spec, i) # :nodoc:
|
||||
orig_spec = spec
|
||||
name = "param#{i}"
|
||||
if spec.is_a?(Hash)
|
||||
name, spec = spec.keys.first, spec.values.first
|
||||
end
|
||||
type = spec
|
||||
if spec.is_a?(Array)
|
||||
ArrayType.new(orig_spec, canonical_signature_entry(spec[0], 0), name)
|
||||
else
|
||||
type = canonical_type(type)
|
||||
if type.is_a?(Symbol)
|
||||
BaseType.new(orig_spec, type, name)
|
||||
else
|
||||
StructuredType.new(orig_spec, type, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def canonical_type(type) # :nodoc:
|
||||
type_name = symbol_name(type) || class_to_type_name(type)
|
||||
type = type_name || type
|
||||
return canonical_type_name(type) if type.is_a?(Symbol)
|
||||
type
|
||||
end
|
||||
|
||||
def canonical_type_name(name) # :nodoc:
|
||||
name = name.to_sym
|
||||
case name
|
||||
when :int, :integer, :fixnum, :bignum
|
||||
:int
|
||||
when :string, :text
|
||||
:string
|
||||
when :base64, :binary
|
||||
:base64
|
||||
when :bool, :boolean
|
||||
:bool
|
||||
when :float, :double
|
||||
:float
|
||||
when :decimal
|
||||
:decimal
|
||||
when :time, :timestamp
|
||||
:time
|
||||
when :datetime
|
||||
:datetime
|
||||
when :date
|
||||
:date
|
||||
else
|
||||
raise(TypeError, "#{name} is not a valid base type")
|
||||
end
|
||||
end
|
||||
|
||||
def canonical_type_class(type) # :nodoc:
|
||||
type = canonical_type(type)
|
||||
type.is_a?(Symbol) ? type_name_to_class(type) : type
|
||||
end
|
||||
|
||||
def symbol_name(name) # :nodoc:
|
||||
return name.to_sym if name.is_a?(Symbol) || name.is_a?(String)
|
||||
nil
|
||||
end
|
||||
|
||||
def class_to_type_name(klass) # :nodoc:
|
||||
klass = klass.class unless klass.is_a?(Class)
|
||||
if derived_from?(Integer, klass) || derived_from?(Fixnum, klass) || derived_from?(Bignum, klass)
|
||||
:int
|
||||
elsif klass == String
|
||||
:string
|
||||
elsif klass == Base64
|
||||
:base64
|
||||
elsif klass == TrueClass || klass == FalseClass
|
||||
:bool
|
||||
elsif derived_from?(Float, klass) || derived_from?(Precision, klass) || derived_from?(Numeric, klass)
|
||||
:float
|
||||
elsif klass == Time
|
||||
:time
|
||||
elsif klass == DateTime
|
||||
:datetime
|
||||
elsif klass == Date
|
||||
:date
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def type_name_to_class(name) # :nodoc:
|
||||
case canonical_type_name(name)
|
||||
when :int
|
||||
Integer
|
||||
when :string
|
||||
String
|
||||
when :base64
|
||||
Base64
|
||||
when :bool
|
||||
TrueClass
|
||||
when :float
|
||||
Float
|
||||
when :decimal
|
||||
BigDecimal
|
||||
when :time
|
||||
Time
|
||||
when :date
|
||||
Date
|
||||
when :datetime
|
||||
DateTime
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def derived_from?(ancestor, child) # :nodoc:
|
||||
child.ancestors.include?(ancestor)
|
||||
end
|
||||
|
||||
module_function :type_name_to_class
|
||||
module_function :class_to_type_name
|
||||
module_function :symbol_name
|
||||
module_function :canonical_type_class
|
||||
module_function :canonical_type_name
|
||||
module_function :canonical_type
|
||||
module_function :canonical_signature_entry
|
||||
module_function :canonical_signature
|
||||
module_function :derived_from?
|
||||
end
|
||||
|
||||
class BaseType # :nodoc:
|
||||
include SignatureTypes
|
||||
|
||||
attr :spec
|
||||
attr :type
|
||||
attr :type_class
|
||||
attr :name
|
||||
|
||||
def initialize(spec, type, name)
|
||||
@spec = spec
|
||||
@type = canonical_type(type)
|
||||
@type_class = canonical_type_class(@type)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def custom?
|
||||
false
|
||||
end
|
||||
|
||||
def array?
|
||||
false
|
||||
end
|
||||
|
||||
def structured?
|
||||
false
|
||||
end
|
||||
|
||||
def human_name(show_name=true)
|
||||
type_type = array? ? element_type.type.to_s : self.type.to_s
|
||||
str = array? ? (type_type + '[]') : type_type
|
||||
show_name ? (str + " " + name.to_s) : str
|
||||
end
|
||||
end
|
||||
|
||||
class ArrayType < BaseType # :nodoc:
|
||||
attr :element_type
|
||||
|
||||
def initialize(spec, element_type, name)
|
||||
super(spec, Array, name)
|
||||
@element_type = element_type
|
||||
end
|
||||
|
||||
def custom?
|
||||
true
|
||||
end
|
||||
|
||||
def array?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class StructuredType < BaseType # :nodoc:
|
||||
def each_member
|
||||
if @type_class.respond_to?(:members)
|
||||
@type_class.members.each do |name, type|
|
||||
yield name, type
|
||||
end
|
||||
elsif @type_class.respond_to?(:columns)
|
||||
i = -1
|
||||
@type_class.columns.each do |column|
|
||||
yield column.name, canonical_signature_entry(column.type, i += 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def custom?
|
||||
true
|
||||
end
|
||||
|
||||
def structured?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class Base64 < String # :nodoc:
|
||||
end
|
||||
end
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title><%= @scaffold_class.wsdl_service_name %> Web Service</title>
|
||||
<style>
|
||||
body { background-color: #fff; color: #333; }
|
||||
|
||||
body, p, ol, ul, td {
|
||||
font-family: verdana, arial, helvetica, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #eee;
|
||||
padding: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
a { color: #000; }
|
||||
a:visited { color: #666; }
|
||||
a:hover { color: #fff; background-color:#000; }
|
||||
|
||||
.fieldWithErrors {
|
||||
padding: 2px;
|
||||
background-color: red;
|
||||
display: table;
|
||||
}
|
||||
|
||||
#errorExplanation {
|
||||
width: 400px;
|
||||
border: 2px solid red;
|
||||
padding: 7px;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
#errorExplanation h2 {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
padding: 5px 5px 5px 15px;
|
||||
font-size: 12px;
|
||||
margin: -7px;
|
||||
background-color: #c00;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#errorExplanation p {
|
||||
color: #333;
|
||||
margin-bottom: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#errorExplanation ul li {
|
||||
font-size: 12px;
|
||||
list-style: square;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<%= @content_for_layout %>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Whitespace-only changes.
-6
@@ -1,6 +0,0 @@
|
||||
<% @scaffold_container.services.each do |service| %>
|
||||
|
||||
<h4>API Methods for <%= service %></h4>
|
||||
<%= service_method_list(service) %>
|
||||
|
||||
<% end %>
|
||||
Whitespace-only changes.
-29
@@ -1,29 +0,0 @@
|
||||
<h4>Method Invocation Details for <em><%= @scaffold_service %>#<%= @scaffold_method.public_name %></em></h4>
|
||||
|
||||
<% form_tag(:action => @scaffold_action_name + '_submit') do -%>
|
||||
<%= hidden_field_tag "service", @scaffold_service.name %>
|
||||
<%= hidden_field_tag "method", @scaffold_method.public_name %>
|
||||
|
||||
<p>
|
||||
<label for="protocol">Protocol:</label><br />
|
||||
<%= select_tag 'protocol', options_for_select([['SOAP', 'soap'], ['XML-RPC', 'xmlrpc']], params['protocol']) %>
|
||||
</p>
|
||||
|
||||
<% if @scaffold_method.expects %>
|
||||
|
||||
<strong>Method Parameters:</strong><br />
|
||||
<% @scaffold_method.expects.each_with_index do |type, i| %>
|
||||
<p>
|
||||
<label for="method_params[<%= i %>]"><%= method_parameter_label(type.name, type) %> </label><br />
|
||||
<%= method_parameter_input_fields(@scaffold_method, type, "method_params", i) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<%= submit_tag "Invoke" %>
|
||||
<% end -%>
|
||||
|
||||
<p>
|
||||
<%= link_to "Back", :action => @scaffold_action_name %>
|
||||
</p>
|
||||
Vendored
Whitespace-only changes.
-30
@@ -1,30 +0,0 @@
|
||||
<h4>Method Invocation Result for <em><%= @scaffold_service %>#<%= @scaffold_method.public_name %></em></h4>
|
||||
|
||||
<p>
|
||||
Invocation took <tt><%= '%f' % @method_elapsed %></tt> seconds
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Return Value:</strong><br />
|
||||
<pre>
|
||||
<%= h @method_return_value.inspect %>
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Request XML:</strong><br />
|
||||
<pre>
|
||||
<%= h @method_request_xml %>
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Response XML:</strong><br />
|
||||
<pre>
|
||||
<%= h @method_response_xml %>
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= link_to "Back", :action => @scaffold_action_name + '_method_params', :method => @scaffold_method.public_name, :service => @scaffold_service.name %>
|
||||
</p>
|
||||
Whitespace-only changes.
@@ -1,110 +0,0 @@
|
||||
require 'test/unit'
|
||||
|
||||
module Test # :nodoc:
|
||||
module Unit # :nodoc:
|
||||
class TestCase # :nodoc:
|
||||
private
|
||||
# invoke the specified API method
|
||||
def invoke_direct(method_name, *args)
|
||||
prepare_request('api', 'api', method_name, *args)
|
||||
@controller.process(@request, @response)
|
||||
decode_rpc_response
|
||||
end
|
||||
alias_method :invoke, :invoke_direct
|
||||
|
||||
# invoke the specified API method on the specified service
|
||||
def invoke_delegated(service_name, method_name, *args)
|
||||
prepare_request(service_name.to_s, service_name, method_name, *args)
|
||||
@controller.process(@request, @response)
|
||||
decode_rpc_response
|
||||
end
|
||||
|
||||
# invoke the specified layered API method on the correct service
|
||||
def invoke_layered(service_name, method_name, *args)
|
||||
prepare_request('api', service_name, method_name, *args)
|
||||
@controller.process(@request, @response)
|
||||
decode_rpc_response
|
||||
end
|
||||
|
||||
# ---------------------- internal ---------------------------
|
||||
|
||||
def prepare_request(action, service_name, api_method_name, *args)
|
||||
@request.recycle!
|
||||
@request.request_parameters['action'] = action
|
||||
@request.env['REQUEST_METHOD'] = 'POST'
|
||||
@request.env['HTTP_CONTENT_TYPE'] = 'text/xml'
|
||||
@request.env['RAW_POST_DATA'] = encode_rpc_call(service_name, api_method_name, *args)
|
||||
case protocol
|
||||
when ActionWebService::Protocol::Soap::SoapProtocol
|
||||
soap_action = "/#{@controller.controller_name}/#{service_name}/#{public_method_name(service_name, api_method_name)}"
|
||||
@request.env['HTTP_SOAPACTION'] = soap_action
|
||||
when ActionWebService::Protocol::XmlRpc::XmlRpcProtocol
|
||||
@request.env.delete('HTTP_SOAPACTION')
|
||||
end
|
||||
end
|
||||
|
||||
def encode_rpc_call(service_name, api_method_name, *args)
|
||||
case @controller.web_service_dispatching_mode
|
||||
when :direct
|
||||
api = @controller.class.web_service_api
|
||||
when :delegated, :layered
|
||||
api = @controller.web_service_object(service_name.to_sym).class.web_service_api
|
||||
end
|
||||
protocol.register_api(api)
|
||||
method = api.api_methods[api_method_name.to_sym]
|
||||
raise ArgumentError, "wrong number of arguments for rpc call (#{args.length} for #{method.expects.length})" if method && method.expects && args.length != method.expects.length
|
||||
protocol.encode_request(public_method_name(service_name, api_method_name), args.dup, method.expects)
|
||||
end
|
||||
|
||||
def decode_rpc_response
|
||||
public_method_name, return_value = protocol.decode_response(@response.body)
|
||||
exception = is_exception?(return_value)
|
||||
raise exception if exception
|
||||
return_value
|
||||
end
|
||||
|
||||
def public_method_name(service_name, api_method_name)
|
||||
public_name = service_api(service_name).public_api_method_name(api_method_name)
|
||||
if @controller.web_service_dispatching_mode == :layered && protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol)
|
||||
'%s.%s' % [service_name.to_s, public_name]
|
||||
else
|
||||
public_name
|
||||
end
|
||||
end
|
||||
|
||||
def service_api(service_name)
|
||||
case @controller.web_service_dispatching_mode
|
||||
when :direct
|
||||
@controller.class.web_service_api
|
||||
when :delegated, :layered
|
||||
@controller.web_service_object(service_name.to_sym).class.web_service_api
|
||||
end
|
||||
end
|
||||
|
||||
def protocol
|
||||
if @protocol.nil?
|
||||
@protocol ||= ActionWebService::Protocol::Soap::SoapProtocol.create(@controller)
|
||||
else
|
||||
case @protocol
|
||||
when :xmlrpc
|
||||
@protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.create(@controller)
|
||||
when :soap
|
||||
@protocol = ActionWebService::Protocol::Soap::SoapProtocol.create(@controller)
|
||||
else
|
||||
@protocol
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def is_exception?(obj)
|
||||
case protocol
|
||||
when :soap, ActionWebService::Protocol::Soap::SoapProtocol
|
||||
(obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && \
|
||||
obj.detail.cause.is_a?(Exception)) ? obj.detail.cause : nil
|
||||
when :xmlrpc, ActionWebService::Protocol::XmlRpc::XmlRpcProtocol
|
||||
obj.is_a?(XMLRPC::FaultException) ? obj : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
module ActionWebService
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 1
|
||||
MINOR = 2
|
||||
TINY = 5
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
end
|
||||
@@ -1 +0,0 @@
|
||||
require 'action_web_service'
|
||||
-1379
File diff suppressed because it is too large.
Load diff
@@ -29,7 +29,7 @@ module Redmine
|
||||
send :include, Redmine::Acts::ActivityProvider::InstanceMethods
|
||||
end
|
||||
|
||||
options.assert_valid_keys(:type, :permission, :timestamp, :find_options)
|
||||
options.assert_valid_keys(:type, :permission, :timestamp, :author_key, :find_options)
|
||||
self.activity_provider_options ||= {}
|
||||
|
||||
# One model can provide different event types
|
||||
@@ -39,6 +39,7 @@ module Redmine
|
||||
options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless options.has_key?(:permission)
|
||||
options[:timestamp] ||= "#{table_name}.created_on"
|
||||
options[:find_options] ||= {}
|
||||
options[:author_key] = "#{table_name}.#{options[:author_key]}" if options[:author_key].is_a?(Symbol)
|
||||
self.activity_provider_options[event_type] = options
|
||||
end
|
||||
end
|
||||
@@ -54,11 +55,25 @@ module Redmine
|
||||
provider_options = activity_provider_options[event_type]
|
||||
raise "#{self.name} can not provide #{event_type} events." if provider_options.nil?
|
||||
|
||||
cond = ARCondition.new(["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to])
|
||||
scope_options = {}
|
||||
cond = ARCondition.new
|
||||
if from && to
|
||||
cond.add(["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to])
|
||||
end
|
||||
if options[:author]
|
||||
return [] if provider_options[:author_key].nil?
|
||||
cond.add(["#{provider_options[:author_key]} = ?", options[:author].id])
|
||||
end
|
||||
cond.add(Project.allowed_to_condition(user, provider_options[:permission], options)) if provider_options[:permission]
|
||||
scope_options[:conditions] = cond.conditions
|
||||
if options[:limit]
|
||||
# id and creation time should be in same order in most cases
|
||||
scope_options[:order] = "#{table_name}.id DESC"
|
||||
scope_options[:limit] = options[:limit]
|
||||
end
|
||||
|
||||
with_scope(:find => { :conditions => cond.conditions }) do
|
||||
find(:all, provider_options[:find_options])
|
||||
with_scope(:find => scope_options) do
|
||||
find(:all, provider_options[:find_options].dup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
require File.dirname(__FILE__) + '/lib/acts_as_attachable'
|
||||
ActiveRecord::Base.send(:include, Redmine::Acts::Attachable)
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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.
|
||||
|
||||
module Redmine
|
||||
module Acts
|
||||
module Attachable
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def acts_as_attachable(options = {})
|
||||
cattr_accessor :attachable_options
|
||||
self.attachable_options = {}
|
||||
attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
|
||||
attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
|
||||
|
||||
has_many :attachments, options.merge(:as => :container,
|
||||
:order => "#{Attachment.table_name}.created_on",
|
||||
:dependent => :destroy)
|
||||
send :include, Redmine::Acts::Attachable::InstanceMethods
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
def attachments_visible?(user=User.current)
|
||||
user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
|
||||
end
|
||||
|
||||
def attachments_deletable?(user=User.current)
|
||||
user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -14,6 +14,8 @@ module Redmine
|
||||
class_eval do
|
||||
has_many :watchers, :as => :watchable, :dependent => :delete_all
|
||||
has_many :watcher_users, :through => :watchers, :source => :user
|
||||
|
||||
attr_protected :watcher_ids, :watcher_user_ids
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Vendored
+1
-2
@@ -1,4 +1,4 @@
|
||||
Copyright (C) 2005 Leon Breedt
|
||||
Copyright (c) 2007 [name of plugin creator]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
@@ -18,4 +18,3 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
= AwesomeNestedSet
|
||||
|
||||
Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is replacement for acts_as_nested_set and BetterNestedSet, but awesomer.
|
||||
|
||||
== What makes this so awesome?
|
||||
|
||||
This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support.
|
||||
|
||||
== Installation
|
||||
|
||||
If you are on Rails 2.1 or later:
|
||||
|
||||
script/plugin install git://github.com/collectiveidea/awesome_nested_set.git
|
||||
|
||||
== Usage
|
||||
|
||||
To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id:
|
||||
|
||||
class CreateCategories < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :categories do |t|
|
||||
t.string :name
|
||||
t.integer :parent_id
|
||||
t.integer :lft
|
||||
t.integer :rgt
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :categories
|
||||
end
|
||||
end
|
||||
|
||||
Enable the nested set functionality by declaring acts_as_nested_set on your model
|
||||
|
||||
class Category < ActiveRecord::Base
|
||||
acts_as_nested_set
|
||||
end
|
||||
|
||||
Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet::SingletonMethods for more info.
|
||||
|
||||
== View Helper
|
||||
|
||||
The view helper is called #nested_set_options.
|
||||
|
||||
Example usage:
|
||||
|
||||
<%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
|
||||
|
||||
<%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
|
||||
|
||||
See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers.
|
||||
|
||||
== References
|
||||
|
||||
You can learn more about nested sets at:
|
||||
|
||||
http://www.dbmsmag.com/9603d06.html
|
||||
http://threebit.net/tutorials/nestedset/tutorial1.html
|
||||
http://api.rubyonrails.com/classes/ActiveRecord/Acts/NestedSet/ClassMethods.html
|
||||
http://opensource.symetrie.com/trac/better_nested_set/
|
||||
|
||||
|
||||
Copyright (c) 2008 Collective Idea, released under the MIT license
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rcov/rcovtask'
|
||||
require "load_multi_rails_rake_tasks"
|
||||
|
||||
spec = eval(File.read("#{File.dirname(__FILE__)}/awesome_nested_set.gemspec"))
|
||||
PKG_NAME = spec.name
|
||||
PKG_VERSION = spec.version
|
||||
|
||||
Rake::GemPackageTask.new(spec) do |pkg|
|
||||
pkg.need_zip = true
|
||||
pkg.need_tar = true
|
||||
end
|
||||
|
||||
|
||||
desc 'Default: run unit tests.'
|
||||
task :default => :test
|
||||
|
||||
desc 'Test the awesome_nested_set plugin.'
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
t.libs << 'lib'
|
||||
t.pattern = 'test/**/*_test.rb'
|
||||
t.verbose = true
|
||||
end
|
||||
|
||||
desc 'Generate documentation for the awesome_nested_set plugin.'
|
||||
Rake::RDocTask.new(:rdoc) do |rdoc|
|
||||
rdoc.rdoc_dir = 'rdoc'
|
||||
rdoc.title = 'AwesomeNestedSet'
|
||||
rdoc.options << '--line-numbers' << '--inline-source'
|
||||
rdoc.rdoc_files.include('README.rdoc')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
end
|
||||
|
||||
namespace :test do
|
||||
desc "just rcov minus html output"
|
||||
Rcov::RcovTask.new(:coverage) do |t|
|
||||
# t.libs << 'test'
|
||||
t.test_files = FileList['test/**/*_test.rb']
|
||||
t.output_dir = 'coverage'
|
||||
t.verbose = true
|
||||
t.rcov_opts = %w(--exclude test,/usr/lib/ruby,/Library/Ruby,lib/awesome_nested_set/named_scope.rb --sort coverage)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "awesome_nested_set"
|
||||
s.version = "1.1.1"
|
||||
s.summary = "An awesome replacement for acts_as_nested_set and better_nested_set."
|
||||
s.description = s.summary
|
||||
|
||||
s.files = %w(init.rb MIT-LICENSE Rakefile README.rdoc lib/awesome_nested_set.rb lib/awesome_nested_set/compatability.rb lib/awesome_nested_set/helper.rb lib/awesome_nested_set/named_scope.rb rails/init.rb test/awesome_nested_set_test.rb test/test_helper.rb test/awesome_nested_set/helper_test.rb test/db/database.yml test/db/schema.rb test/fixtures/categories.yml test/fixtures/category.rb test/fixtures/departments.yml test/fixtures/notes.yml)
|
||||
|
||||
s.add_dependency "activerecord", ['>= 1.1']
|
||||
|
||||
s.has_rdoc = true
|
||||
s.extra_rdoc_files = [ "README.rdoc"]
|
||||
s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
|
||||
|
||||
s.test_files = %w(test/awesome_nested_set_test.rb test/test_helper.rb test/awesome_nested_set/helper_test.rb test/db/database.yml test/db/schema.rb test/fixtures/categories.yml test/fixtures/category.rb test/fixtures/departments.yml test/fixtures/notes.yml)
|
||||
s.require_path = 'lib'
|
||||
s.author = "Collective Idea"
|
||||
s.email = "info@collectiveidea.com"
|
||||
s.homepage = "http://collectiveidea.com"
|
||||
end
|
||||
+1
@@ -0,0 +1 @@
|
||||
require File.dirname(__FILE__) + "/rails/init"
|
||||
@@ -0,0 +1,547 @@
|
||||
module CollectiveIdea #:nodoc:
|
||||
module Acts #:nodoc:
|
||||
module NestedSet #:nodoc:
|
||||
def self.included(base)
|
||||
base.extend(SingletonMethods)
|
||||
end
|
||||
|
||||
# This acts provides Nested Set functionality. Nested Set is a smart way to implement
|
||||
# an _ordered_ tree, with the added feature that you can select the children and all of their
|
||||
# descendants with a single query. The drawback is that insertion or move need some complex
|
||||
# sql queries. But everything is done here by this module!
|
||||
#
|
||||
# Nested sets are appropriate each time you want either an orderd tree (menus,
|
||||
# commercial categories) or an efficient way of querying big trees (threaded posts).
|
||||
#
|
||||
# == API
|
||||
#
|
||||
# Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
|
||||
# by another easier, except for the creation:
|
||||
#
|
||||
# in acts_as_tree:
|
||||
# item.children.create(:name => "child1")
|
||||
#
|
||||
# in acts_as_nested_set:
|
||||
# # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
|
||||
# child = MyClass.new(:name => "child1")
|
||||
# child.save
|
||||
# # now move the item to its right place
|
||||
# child.move_to_child_of my_item
|
||||
#
|
||||
# You can pass an id or an object to:
|
||||
# * <tt>#move_to_child_of</tt>
|
||||
# * <tt>#move_to_right_of</tt>
|
||||
# * <tt>#move_to_left_of</tt>
|
||||
#
|
||||
module SingletonMethods
|
||||
# Configuration options are:
|
||||
#
|
||||
# * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
|
||||
# * +:left_column+ - column name for left boundry data, default "lft"
|
||||
# * +:right_column+ - column name for right boundry data, default "rgt"
|
||||
# * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
|
||||
# (if it hasn't been already) and use that as the foreign key restriction. You
|
||||
# can also pass an array to scope by multiple attributes.
|
||||
# Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
|
||||
# * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
|
||||
# child objects are destroyed alongside this object by calling their destroy
|
||||
# method. If set to :delete_all (default), all the child objects are deleted
|
||||
# without calling their destroy method.
|
||||
#
|
||||
# See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
|
||||
# CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
|
||||
# to acts_as_nested_set models
|
||||
def acts_as_nested_set(options = {})
|
||||
options = {
|
||||
:parent_column => 'parent_id',
|
||||
:left_column => 'lft',
|
||||
:right_column => 'rgt',
|
||||
:order => 'id',
|
||||
:dependent => :delete_all, # or :destroy
|
||||
}.merge(options)
|
||||
|
||||
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
|
||||
options[:scope] = "#{options[:scope]}_id".intern
|
||||
end
|
||||
|
||||
write_inheritable_attribute :acts_as_nested_set_options, options
|
||||
class_inheritable_reader :acts_as_nested_set_options
|
||||
|
||||
include Comparable
|
||||
include Columns
|
||||
include InstanceMethods
|
||||
extend Columns
|
||||
extend ClassMethods
|
||||
|
||||
# no bulk assignment
|
||||
attr_protected left_column_name.intern,
|
||||
right_column_name.intern,
|
||||
parent_column_name.intern
|
||||
|
||||
before_create :set_default_left_and_right
|
||||
before_destroy :prune_from_tree
|
||||
|
||||
# no assignment to structure fields
|
||||
[left_column_name, right_column_name, parent_column_name].each do |column|
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def #{column}=(x)
|
||||
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
|
||||
named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
|
||||
if self.respond_to?(:define_callbacks)
|
||||
define_callbacks("before_move", "after_move")
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
# Returns the first root
|
||||
def root
|
||||
roots.find(:first)
|
||||
end
|
||||
|
||||
def valid?
|
||||
left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
|
||||
end
|
||||
|
||||
def left_and_rights_valid?
|
||||
count(
|
||||
:joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
|
||||
"#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
|
||||
:conditions =>
|
||||
"#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
|
||||
"#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
|
||||
"#{quoted_table_name}.#{quoted_left_column_name} >= " +
|
||||
"#{quoted_table_name}.#{quoted_right_column_name} OR " +
|
||||
"(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
|
||||
"(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
|
||||
"#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
|
||||
) == 0
|
||||
end
|
||||
|
||||
def no_duplicates_for_columns?
|
||||
scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
|
||||
connection.quote_column_name(c)
|
||||
end.push(nil).join(", ")
|
||||
[quoted_left_column_name, quoted_right_column_name].all? do |column|
|
||||
# No duplicates
|
||||
find(:first,
|
||||
:select => "#{scope_string}#{column}, COUNT(#{column})",
|
||||
:group => "#{scope_string}#{column}
|
||||
HAVING COUNT(#{column}) > 1").nil?
|
||||
end
|
||||
end
|
||||
|
||||
# Wrapper for each_root_valid? that can deal with scope.
|
||||
def all_roots_valid?
|
||||
if acts_as_nested_set_options[:scope]
|
||||
roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
|
||||
each_root_valid?(grouped_roots)
|
||||
end
|
||||
else
|
||||
each_root_valid?(roots)
|
||||
end
|
||||
end
|
||||
|
||||
def each_root_valid?(roots_to_validate)
|
||||
left = right = 0
|
||||
roots_to_validate.all? do |root|
|
||||
returning(root.left > left && root.right > right) do
|
||||
left = root.left
|
||||
right = root.right
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
|
||||
def rebuild!
|
||||
# Don't rebuild a valid tree.
|
||||
return true if valid?
|
||||
|
||||
scope = lambda{}
|
||||
if acts_as_nested_set_options[:scope]
|
||||
scope = lambda{|node|
|
||||
scope_column_names.inject(""){|str, column_name|
|
||||
str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
|
||||
}
|
||||
}
|
||||
end
|
||||
indices = {}
|
||||
|
||||
set_left_and_rights = lambda do |node|
|
||||
# set left
|
||||
node[left_column_name] = indices[scope.call(node)] += 1
|
||||
# find
|
||||
find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
|
||||
# set right
|
||||
node[right_column_name] = indices[scope.call(node)] += 1
|
||||
node.save!
|
||||
end
|
||||
|
||||
# Find root node(s)
|
||||
root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
|
||||
# setup index for this scope
|
||||
indices[scope.call(root_node)] ||= 0
|
||||
set_left_and_rights.call(root_node)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Mixed into both classes and instances to provide easy access to the column names
|
||||
module Columns
|
||||
def left_column_name
|
||||
acts_as_nested_set_options[:left_column]
|
||||
end
|
||||
|
||||
def right_column_name
|
||||
acts_as_nested_set_options[:right_column]
|
||||
end
|
||||
|
||||
def parent_column_name
|
||||
acts_as_nested_set_options[:parent_column]
|
||||
end
|
||||
|
||||
def scope_column_names
|
||||
Array(acts_as_nested_set_options[:scope])
|
||||
end
|
||||
|
||||
def quoted_left_column_name
|
||||
connection.quote_column_name(left_column_name)
|
||||
end
|
||||
|
||||
def quoted_right_column_name
|
||||
connection.quote_column_name(right_column_name)
|
||||
end
|
||||
|
||||
def quoted_parent_column_name
|
||||
connection.quote_column_name(parent_column_name)
|
||||
end
|
||||
|
||||
def quoted_scope_column_names
|
||||
scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
|
||||
end
|
||||
end
|
||||
|
||||
# Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
|
||||
#
|
||||
# category.self_and_descendants.count
|
||||
# category.ancestors.find(:all, :conditions => "name like '%foo%'")
|
||||
module InstanceMethods
|
||||
# Value of the parent column
|
||||
def parent_id
|
||||
self[parent_column_name]
|
||||
end
|
||||
|
||||
# Value of the left column
|
||||
def left
|
||||
self[left_column_name]
|
||||
end
|
||||
|
||||
# Value of the right column
|
||||
def right
|
||||
self[right_column_name]
|
||||
end
|
||||
|
||||
# Returns true if this is a root node.
|
||||
def root?
|
||||
parent_id.nil?
|
||||
end
|
||||
|
||||
def leaf?
|
||||
right - left == 1
|
||||
end
|
||||
|
||||
# Returns true is this is a child node
|
||||
def child?
|
||||
!parent_id.nil?
|
||||
end
|
||||
|
||||
# order by left column
|
||||
def <=>(x)
|
||||
left <=> x.left
|
||||
end
|
||||
|
||||
# Redefine to act like active record
|
||||
def ==(comparison_object)
|
||||
comparison_object.equal?(self) ||
|
||||
(comparison_object.instance_of?(self.class) &&
|
||||
comparison_object.id == id &&
|
||||
!comparison_object.new_record?)
|
||||
end
|
||||
|
||||
# Returns root
|
||||
def root
|
||||
self_and_ancestors.find(:first)
|
||||
end
|
||||
|
||||
# Returns the immediate parent
|
||||
def parent
|
||||
nested_set_scope.find_by_id(parent_id) if parent_id
|
||||
end
|
||||
|
||||
# Returns the array of all parents and self
|
||||
def self_and_ancestors
|
||||
nested_set_scope.scoped :conditions => [
|
||||
"#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
|
||||
]
|
||||
end
|
||||
|
||||
# Returns an array of all parents
|
||||
def ancestors
|
||||
without_self self_and_ancestors
|
||||
end
|
||||
|
||||
# Returns the array of all children of the parent, including self
|
||||
def self_and_siblings
|
||||
nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
|
||||
end
|
||||
|
||||
# Returns the array of all children of the parent, except self
|
||||
def siblings
|
||||
without_self self_and_siblings
|
||||
end
|
||||
|
||||
# Returns a set of all of its nested children which do not have children
|
||||
def leaves
|
||||
descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
|
||||
end
|
||||
|
||||
# Returns the level of this object in the tree
|
||||
# root level is 0
|
||||
def level
|
||||
parent_id.nil? ? 0 : ancestors.count
|
||||
end
|
||||
|
||||
# Returns a set of itself and all of its nested children
|
||||
def self_and_descendants
|
||||
nested_set_scope.scoped :conditions => [
|
||||
"#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
|
||||
]
|
||||
end
|
||||
|
||||
# Returns a set of all of its children and nested children
|
||||
def descendants
|
||||
without_self self_and_descendants
|
||||
end
|
||||
|
||||
# Returns a set of only this entry's immediate children
|
||||
def children
|
||||
nested_set_scope.scoped :conditions => {parent_column_name => self}
|
||||
end
|
||||
|
||||
def is_descendant_of?(other)
|
||||
other.left < self.left && self.left < other.right && same_scope?(other)
|
||||
end
|
||||
|
||||
def is_or_is_descendant_of?(other)
|
||||
other.left <= self.left && self.left < other.right && same_scope?(other)
|
||||
end
|
||||
|
||||
def is_ancestor_of?(other)
|
||||
self.left < other.left && other.left < self.right && same_scope?(other)
|
||||
end
|
||||
|
||||
def is_or_is_ancestor_of?(other)
|
||||
self.left <= other.left && other.left < self.right && same_scope?(other)
|
||||
end
|
||||
|
||||
# Check if other model is in the same scope
|
||||
def same_scope?(other)
|
||||
Array(acts_as_nested_set_options[:scope]).all? do |attr|
|
||||
self.send(attr) == other.send(attr)
|
||||
end
|
||||
end
|
||||
|
||||
# Find the first sibling to the left
|
||||
def left_sibling
|
||||
siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
|
||||
:order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
|
||||
end
|
||||
|
||||
# Find the first sibling to the right
|
||||
def right_sibling
|
||||
siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
|
||||
end
|
||||
|
||||
# Shorthand method for finding the left sibling and moving to the left of it.
|
||||
def move_left
|
||||
move_to_left_of left_sibling
|
||||
end
|
||||
|
||||
# Shorthand method for finding the right sibling and moving to the right of it.
|
||||
def move_right
|
||||
move_to_right_of right_sibling
|
||||
end
|
||||
|
||||
# Move the node to the left of another node (you can pass id only)
|
||||
def move_to_left_of(node)
|
||||
move_to node, :left
|
||||
end
|
||||
|
||||
# Move the node to the left of another node (you can pass id only)
|
||||
def move_to_right_of(node)
|
||||
move_to node, :right
|
||||
end
|
||||
|
||||
# Move the node to the child of another node (you can pass id only)
|
||||
def move_to_child_of(node)
|
||||
move_to node, :child
|
||||
end
|
||||
|
||||
# Move the node to root nodes
|
||||
def move_to_root
|
||||
move_to nil, :root
|
||||
end
|
||||
|
||||
def move_possible?(target)
|
||||
self != target && # Can't target self
|
||||
same_scope?(target) && # can't be in different scopes
|
||||
# !(left..right).include?(target.left..target.right) # this needs tested more
|
||||
# detect impossible move
|
||||
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
|
||||
end
|
||||
|
||||
def to_text
|
||||
self_and_descendants.map do |node|
|
||||
"#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
|
||||
end.join("\n")
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def without_self(scope)
|
||||
scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
|
||||
end
|
||||
|
||||
# All nested set queries should use this nested_set_scope, which performs finds on
|
||||
# the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
|
||||
# declaration.
|
||||
def nested_set_scope
|
||||
options = {:order => quoted_left_column_name}
|
||||
scopes = Array(acts_as_nested_set_options[:scope])
|
||||
options[:conditions] = scopes.inject({}) do |conditions,attr|
|
||||
conditions.merge attr => self[attr]
|
||||
end unless scopes.empty?
|
||||
self.class.base_class.scoped options
|
||||
end
|
||||
|
||||
# on creation, set automatically lft and rgt to the end of the tree
|
||||
def set_default_left_and_right
|
||||
maxright = nested_set_scope.maximum(right_column_name) || 0
|
||||
# adds the new node to the right of all existing nodes
|
||||
self[left_column_name] = maxright + 1
|
||||
self[right_column_name] = maxright + 2
|
||||
end
|
||||
|
||||
# Prunes a branch off of the tree, shifting all of the elements on the right
|
||||
# back to the left so the counts still work.
|
||||
def prune_from_tree
|
||||
return if right.nil? || left.nil?
|
||||
diff = right - left + 1
|
||||
|
||||
delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
|
||||
:destroy_all : :delete_all
|
||||
|
||||
self.class.base_class.transaction do
|
||||
nested_set_scope.send(delete_method,
|
||||
["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
|
||||
left, right]
|
||||
)
|
||||
nested_set_scope.update_all(
|
||||
["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
|
||||
["#{quoted_left_column_name} >= ?", right]
|
||||
)
|
||||
nested_set_scope.update_all(
|
||||
["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
|
||||
["#{quoted_right_column_name} >= ?", right]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# reload left, right, and parent
|
||||
def reload_nested_set
|
||||
reload(:select => "#{quoted_left_column_name}, " +
|
||||
"#{quoted_right_column_name}, #{quoted_parent_column_name}")
|
||||
end
|
||||
|
||||
def move_to(target, position)
|
||||
raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
|
||||
return if callback(:before_move) == false
|
||||
transaction do
|
||||
if target.is_a? self.class.base_class
|
||||
target.reload_nested_set
|
||||
elsif position != :root
|
||||
# load object if node is not an object
|
||||
target = nested_set_scope.find(target)
|
||||
end
|
||||
self.reload_nested_set
|
||||
|
||||
unless position == :root || move_possible?(target)
|
||||
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
|
||||
end
|
||||
|
||||
bound = case position
|
||||
when :child; target[right_column_name]
|
||||
when :left; target[left_column_name]
|
||||
when :right; target[right_column_name] + 1
|
||||
when :root; 1
|
||||
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
|
||||
end
|
||||
|
||||
if bound > self[right_column_name]
|
||||
bound = bound - 1
|
||||
other_bound = self[right_column_name] + 1
|
||||
else
|
||||
other_bound = self[left_column_name] - 1
|
||||
end
|
||||
|
||||
# there would be no change
|
||||
return if bound == self[right_column_name] || bound == self[left_column_name]
|
||||
|
||||
# we have defined the boundaries of two non-overlapping intervals,
|
||||
# so sorting puts both the intervals and their boundaries in order
|
||||
a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
|
||||
|
||||
new_parent = case position
|
||||
when :child; target.id
|
||||
when :root; nil
|
||||
else target[parent_column_name]
|
||||
end
|
||||
|
||||
self.class.base_class.update_all([
|
||||
"#{quoted_left_column_name} = CASE " +
|
||||
"WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
|
||||
"THEN #{quoted_left_column_name} + :d - :b " +
|
||||
"WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
|
||||
"THEN #{quoted_left_column_name} + :a - :c " +
|
||||
"ELSE #{quoted_left_column_name} END, " +
|
||||
"#{quoted_right_column_name} = CASE " +
|
||||
"WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
|
||||
"THEN #{quoted_right_column_name} + :d - :b " +
|
||||
"WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
|
||||
"THEN #{quoted_right_column_name} + :a - :c " +
|
||||
"ELSE #{quoted_right_column_name} END, " +
|
||||
"#{quoted_parent_column_name} = CASE " +
|
||||
"WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
|
||||
"ELSE #{quoted_parent_column_name} END",
|
||||
{:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
|
||||
], nested_set_scope.proxy_options[:conditions])
|
||||
end
|
||||
target.reload_nested_set if target
|
||||
self.reload_nested_set
|
||||
callback(:after_move)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
# Rails <2.x doesn't define #except
|
||||
class Hash #:nodoc:
|
||||
# Returns a new hash without the given keys.
|
||||
def except(*keys)
|
||||
clone.except!(*keys)
|
||||
end unless method_defined?(:except)
|
||||
|
||||
# Replaces the hash without the given keys.
|
||||
def except!(*keys)
|
||||
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
||||
keys.each { |key| delete(key) }
|
||||
self
|
||||
end unless method_defined?(:except!)
|
||||
end
|
||||
|
||||
# NamedScope is new to Rails 2.1
|
||||
unless defined? ActiveRecord::NamedScope
|
||||
require 'awesome_nested_set/named_scope'
|
||||
ActiveRecord::Base.class_eval do
|
||||
include CollectiveIdea::NamedScope
|
||||
end
|
||||
end
|
||||
|
||||
# Rails 1.2.x doesn't define #quoted_table_name
|
||||
class ActiveRecord::Base #:nodoc:
|
||||
def self.quoted_table_name
|
||||
self.connection.quote_column_name(self.table_name)
|
||||
end unless methods.include?('quoted_table_name')
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
module CollectiveIdea #:nodoc:
|
||||
module Acts #:nodoc:
|
||||
module NestedSet #:nodoc:
|
||||
# This module provides some helpers for the model classes using acts_as_nested_set.
|
||||
# It is included by default in all views.
|
||||
#
|
||||
module Helper
|
||||
# Returns options for select.
|
||||
# You can exclude some items from the tree.
|
||||
# You can pass a block receiving an item and returning the string displayed in the select.
|
||||
#
|
||||
# == Params
|
||||
# * +class_or_item+ - Class name or top level times
|
||||
# * +mover+ - The item that is being move, used to exlude impossible moves
|
||||
# * +&block+ - a block that will be used to display: { |item| ... item.name }
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
|
||||
# "#{'–' * i.level} #{i.name}"
|
||||
# }) %>
|
||||
#
|
||||
def nested_set_options(class_or_item, mover = nil)
|
||||
class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
|
||||
items = Array(class_or_item)
|
||||
result = []
|
||||
items.each do |root|
|
||||
result += root.self_and_descendants.map do |i|
|
||||
if mover.nil? || mover.new_record? || mover.move_possible?(i)
|
||||
[yield(i), i.id]
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,140 @@
|
||||
# Taken from Rails 2.1
|
||||
module CollectiveIdea #:nodoc:
|
||||
module NamedScope #:nodoc:
|
||||
# All subclasses of ActiveRecord::Base have two named_scopes:
|
||||
# * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
|
||||
# * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly:
|
||||
#
|
||||
# Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)
|
||||
#
|
||||
# These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
|
||||
# intermediate values (scopes) around as first-class objects is convenient.
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
extend ClassMethods
|
||||
named_scope :scoped, lambda { |scope| scope }
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods #:nodoc:
|
||||
def scopes
|
||||
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
|
||||
end
|
||||
|
||||
# Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
|
||||
# such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :red, :conditions => {:color => 'red'}
|
||||
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
|
||||
# end
|
||||
#
|
||||
# The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
|
||||
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
|
||||
#
|
||||
# Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
|
||||
# constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
|
||||
# <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
|
||||
# as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
|
||||
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
|
||||
#
|
||||
# These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
|
||||
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
|
||||
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
|
||||
#
|
||||
# All scopes are available as class methods on the ActiveRecord descendent upon which the scopes were defined. But they are also available to
|
||||
# <tt>has_many</tt> associations. If,
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# has_many :shirts
|
||||
# end
|
||||
#
|
||||
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
|
||||
# only shirts.
|
||||
#
|
||||
# Named scopes can also be procedural.
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :colored, lambda { |color|
|
||||
# { :conditions => { :color => color } }
|
||||
# }
|
||||
# end
|
||||
#
|
||||
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
|
||||
#
|
||||
# Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :red, :conditions => {:color => 'red'} do
|
||||
# def dom_id
|
||||
# 'red_shirts'
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#
|
||||
# For testing complex named scopes, you can examine the scoping options using the
|
||||
# <tt>proxy_options</tt> method on the proxy itself.
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :colored, lambda { |color|
|
||||
# { :conditions => { :color => color } }
|
||||
# }
|
||||
# end
|
||||
#
|
||||
# expected_options = { :conditions => { :colored => 'red' } }
|
||||
# assert_equal expected_options, Shirt.colored('red').proxy_options
|
||||
def named_scope(name, options = {}, &block)
|
||||
scopes[name] = lambda do |parent_scope, *args|
|
||||
Scope.new(parent_scope, case options
|
||||
when Hash
|
||||
options
|
||||
when Proc
|
||||
options.call(*args)
|
||||
end, &block)
|
||||
end
|
||||
(class << self; self end).instance_eval do
|
||||
define_method name do |*args|
|
||||
scopes[name].call(self, *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Scope #:nodoc:
|
||||
attr_reader :proxy_scope, :proxy_options
|
||||
[].methods.each { |m| delegate m, :to => :proxy_found unless m =~ /(^__|^nil\?|^send|class|extend|find|count|sum|average|maximum|minimum|paginate)/ }
|
||||
delegate :scopes, :with_scope, :to => :proxy_scope
|
||||
|
||||
def initialize(proxy_scope, options, &block)
|
||||
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
|
||||
extend Module.new(&block) if block_given?
|
||||
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
|
||||
end
|
||||
|
||||
def reload
|
||||
load_found; self
|
||||
end
|
||||
|
||||
protected
|
||||
def proxy_found
|
||||
@found || load_found
|
||||
end
|
||||
|
||||
private
|
||||
def method_missing(method, *args, &block)
|
||||
if scopes.include?(method)
|
||||
scopes[method].call(self, *args)
|
||||
else
|
||||
with_scope :find => proxy_options do
|
||||
proxy_scope.send(method, *args, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_found
|
||||
@found = find(:all)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
require 'awesome_nested_set/compatability'
|
||||
require 'awesome_nested_set'
|
||||
|
||||
ActiveRecord::Base.class_eval do
|
||||
include CollectiveIdea::Acts::NestedSet
|
||||
end
|
||||
|
||||
if defined?(ActionView)
|
||||
require 'awesome_nested_set/helper'
|
||||
ActionView::Base.class_eval do
|
||||
include CollectiveIdea::Acts::NestedSet::Helper
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
require File.dirname(__FILE__) + '/../test_helper'
|
||||
|
||||
module CollectiveIdea
|
||||
module Acts #:nodoc:
|
||||
module NestedSet #:nodoc:
|
||||
class AwesomeNestedSetTest < Test::Unit::TestCase
|
||||
include Helper
|
||||
fixtures :categories
|
||||
|
||||
def test_nested_set_options
|
||||
expected = [
|
||||
[" Top Level", 1],
|
||||
["- Child 1", 2],
|
||||
['- Child 2', 3],
|
||||
['-- Child 2.1', 4],
|
||||
['- Child 3', 5],
|
||||
[" Top Level 2", 6]
|
||||
]
|
||||
actual = nested_set_options(Category) do |c|
|
||||
"#{'-' * c.level} #{c.name}"
|
||||
end
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
def test_nested_set_options_with_mover
|
||||
expected = [
|
||||
[" Top Level", 1],
|
||||
["- Child 1", 2],
|
||||
['- Child 3', 5],
|
||||
[" Top Level 2", 6]
|
||||
]
|
||||
actual = nested_set_options(Category, categories(:child_2)) do |c|
|
||||
"#{'-' * c.level} #{c.name}"
|
||||
end
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,603 @@
|
||||
require File.dirname(__FILE__) + '/test_helper'
|
||||
|
||||
class Note < ActiveRecord::Base
|
||||
acts_as_nested_set :scope => [:notable_id, :notable_type]
|
||||
end
|
||||
|
||||
class AwesomeNestedSetTest < Test::Unit::TestCase
|
||||
|
||||
class Default < ActiveRecord::Base
|
||||
acts_as_nested_set
|
||||
set_table_name 'categories'
|
||||
end
|
||||
class Scoped < ActiveRecord::Base
|
||||
acts_as_nested_set :scope => :organization
|
||||
set_table_name 'categories'
|
||||
end
|
||||
|
||||
def test_left_column_default
|
||||
assert_equal 'lft', Default.acts_as_nested_set_options[:left_column]
|
||||
end
|
||||
|
||||
def test_right_column_default
|
||||
assert_equal 'rgt', Default.acts_as_nested_set_options[:right_column]
|
||||
end
|
||||
|
||||
def test_parent_column_default
|
||||
assert_equal 'parent_id', Default.acts_as_nested_set_options[:parent_column]
|
||||
end
|
||||
|
||||
def test_scope_default
|
||||
assert_nil Default.acts_as_nested_set_options[:scope]
|
||||
end
|
||||
|
||||
def test_left_column_name
|
||||
assert_equal 'lft', Default.left_column_name
|
||||
assert_equal 'lft', Default.new.left_column_name
|
||||
end
|
||||
|
||||
def test_right_column_name
|
||||
assert_equal 'rgt', Default.right_column_name
|
||||
assert_equal 'rgt', Default.new.right_column_name
|
||||
end
|
||||
|
||||
def test_parent_column_name
|
||||
assert_equal 'parent_id', Default.parent_column_name
|
||||
assert_equal 'parent_id', Default.new.parent_column_name
|
||||
end
|
||||
|
||||
def test_quoted_left_column_name
|
||||
quoted = Default.connection.quote_column_name('lft')
|
||||
assert_equal quoted, Default.quoted_left_column_name
|
||||
assert_equal quoted, Default.new.quoted_left_column_name
|
||||
end
|
||||
|
||||
def test_quoted_right_column_name
|
||||
quoted = Default.connection.quote_column_name('rgt')
|
||||
assert_equal quoted, Default.quoted_right_column_name
|
||||
assert_equal quoted, Default.new.quoted_right_column_name
|
||||
end
|
||||
|
||||
def test_left_column_protected_from_assignment
|
||||
assert_raises(ActiveRecord::ActiveRecordError) { Category.new.lft = 1 }
|
||||
end
|
||||
|
||||
def test_right_column_protected_from_assignment
|
||||
assert_raises(ActiveRecord::ActiveRecordError) { Category.new.rgt = 1 }
|
||||
end
|
||||
|
||||
def test_parent_column_protected_from_assignment
|
||||
assert_raises(ActiveRecord::ActiveRecordError) { Category.new.parent_id = 1 }
|
||||
end
|
||||
|
||||
def test_colums_protected_on_initialize
|
||||
c = Category.new(:lft => 1, :rgt => 2, :parent_id => 3)
|
||||
assert_nil c.lft
|
||||
assert_nil c.rgt
|
||||
assert_nil c.parent_id
|
||||
end
|
||||
|
||||
def test_scoped_appends_id
|
||||
assert_equal :organization_id, Scoped.acts_as_nested_set_options[:scope]
|
||||
end
|
||||
|
||||
def test_roots_class_method
|
||||
assert_equal Category.find_all_by_parent_id(nil), Category.roots
|
||||
end
|
||||
|
||||
def test_root_class_method
|
||||
assert_equal categories(:top_level), Category.root
|
||||
end
|
||||
|
||||
def test_root
|
||||
assert_equal categories(:top_level), categories(:child_3).root
|
||||
end
|
||||
|
||||
def test_root?
|
||||
assert categories(:top_level).root?
|
||||
assert categories(:top_level_2).root?
|
||||
end
|
||||
|
||||
def test_leaves_class_method
|
||||
assert_equal Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1"), Category.leaves
|
||||
assert_equal Category.leaves.count, 4
|
||||
assert (Category.leaves.include? categories(:child_1))
|
||||
assert (Category.leaves.include? categories(:child_2_1))
|
||||
assert (Category.leaves.include? categories(:child_3))
|
||||
assert (Category.leaves.include? categories(:top_level_2))
|
||||
end
|
||||
|
||||
def test_leaf
|
||||
assert categories(:child_1).leaf?
|
||||
assert categories(:child_2_1).leaf?
|
||||
assert categories(:child_3).leaf?
|
||||
assert categories(:top_level_2).leaf?
|
||||
|
||||
assert !categories(:top_level).leaf?
|
||||
assert !categories(:child_2).leaf?
|
||||
end
|
||||
|
||||
def test_parent
|
||||
assert_equal categories(:child_2), categories(:child_2_1).parent
|
||||
end
|
||||
|
||||
def test_self_and_ancestors
|
||||
child = categories(:child_2_1)
|
||||
self_and_ancestors = [categories(:top_level), categories(:child_2), child]
|
||||
assert_equal self_and_ancestors, child.self_and_ancestors
|
||||
end
|
||||
|
||||
def test_ancestors
|
||||
child = categories(:child_2_1)
|
||||
ancestors = [categories(:top_level), categories(:child_2)]
|
||||
assert_equal ancestors, child.ancestors
|
||||
end
|
||||
|
||||
def test_self_and_siblings
|
||||
child = categories(:child_2)
|
||||
self_and_siblings = [categories(:child_1), child, categories(:child_3)]
|
||||
assert_equal self_and_siblings, child.self_and_siblings
|
||||
assert_nothing_raised do
|
||||
tops = [categories(:top_level), categories(:top_level_2)]
|
||||
assert_equal tops, categories(:top_level).self_and_siblings
|
||||
end
|
||||
end
|
||||
|
||||
def test_siblings
|
||||
child = categories(:child_2)
|
||||
siblings = [categories(:child_1), categories(:child_3)]
|
||||
assert_equal siblings, child.siblings
|
||||
end
|
||||
|
||||
def test_leaves
|
||||
leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3), categories(:top_level_2)]
|
||||
assert categories(:top_level).leaves, leaves
|
||||
end
|
||||
|
||||
def test_level
|
||||
assert_equal 0, categories(:top_level).level
|
||||
assert_equal 1, categories(:child_1).level
|
||||
assert_equal 2, categories(:child_2_1).level
|
||||
end
|
||||
|
||||
def test_has_children?
|
||||
assert categories(:child_2_1).children.empty?
|
||||
assert !categories(:child_2).children.empty?
|
||||
assert !categories(:top_level).children.empty?
|
||||
end
|
||||
|
||||
def test_self_and_descendents
|
||||
parent = categories(:top_level)
|
||||
self_and_descendants = [parent, categories(:child_1), categories(:child_2),
|
||||
categories(:child_2_1), categories(:child_3)]
|
||||
assert_equal self_and_descendants, parent.self_and_descendants
|
||||
assert_equal self_and_descendants, parent.self_and_descendants.count
|
||||
end
|
||||
|
||||
def test_descendents
|
||||
lawyers = Category.create!(:name => "lawyers")
|
||||
us = Category.create!(:name => "United States")
|
||||
us.move_to_child_of(lawyers)
|
||||
patent = Category.create!(:name => "Patent Law")
|
||||
patent.move_to_child_of(us)
|
||||
lawyers.reload
|
||||
|
||||
assert_equal 1, lawyers.children.size
|
||||
assert_equal 1, us.children.size
|
||||
assert_equal 2, lawyers.descendants.size
|
||||
end
|
||||
|
||||
def test_self_and_descendents
|
||||
parent = categories(:top_level)
|
||||
descendants = [categories(:child_1), categories(:child_2),
|
||||
categories(:child_2_1), categories(:child_3)]
|
||||
assert_equal descendants, parent.descendants
|
||||
end
|
||||
|
||||
def test_children
|
||||
category = categories(:top_level)
|
||||
category.children.each {|c| assert_equal category.id, c.parent_id }
|
||||
end
|
||||
|
||||
def test_is_or_is_ancestor_of?
|
||||
assert categories(:top_level).is_or_is_ancestor_of?(categories(:child_1))
|
||||
assert categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1))
|
||||
assert categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1))
|
||||
assert !categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2))
|
||||
assert !categories(:child_1).is_or_is_ancestor_of?(categories(:child_2))
|
||||
assert categories(:child_1).is_or_is_ancestor_of?(categories(:child_1))
|
||||
end
|
||||
|
||||
def test_is_ancestor_of?
|
||||
assert categories(:top_level).is_ancestor_of?(categories(:child_1))
|
||||
assert categories(:top_level).is_ancestor_of?(categories(:child_2_1))
|
||||
assert categories(:child_2).is_ancestor_of?(categories(:child_2_1))
|
||||
assert !categories(:child_2_1).is_ancestor_of?(categories(:child_2))
|
||||
assert !categories(:child_1).is_ancestor_of?(categories(:child_2))
|
||||
assert !categories(:child_1).is_ancestor_of?(categories(:child_1))
|
||||
end
|
||||
|
||||
def test_is_or_is_ancestor_of_with_scope
|
||||
root = Scoped.root
|
||||
child = root.children.first
|
||||
assert root.is_or_is_ancestor_of?(child)
|
||||
child.update_attribute :organization_id, 'different'
|
||||
assert !root.is_or_is_ancestor_of?(child)
|
||||
end
|
||||
|
||||
def test_is_or_is_descendant_of?
|
||||
assert categories(:child_1).is_or_is_descendant_of?(categories(:top_level))
|
||||
assert categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level))
|
||||
assert categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2))
|
||||
assert !categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1))
|
||||
assert !categories(:child_2).is_or_is_descendant_of?(categories(:child_1))
|
||||
assert categories(:child_1).is_or_is_descendant_of?(categories(:child_1))
|
||||
end
|
||||
|
||||
def test_is_descendant_of?
|
||||
assert categories(:child_1).is_descendant_of?(categories(:top_level))
|
||||
assert categories(:child_2_1).is_descendant_of?(categories(:top_level))
|
||||
assert categories(:child_2_1).is_descendant_of?(categories(:child_2))
|
||||
assert !categories(:child_2).is_descendant_of?(categories(:child_2_1))
|
||||
assert !categories(:child_2).is_descendant_of?(categories(:child_1))
|
||||
assert !categories(:child_1).is_descendant_of?(categories(:child_1))
|
||||
end
|
||||
|
||||
def test_is_or_is_descendant_of_with_scope
|
||||
root = Scoped.root
|
||||
child = root.children.first
|
||||
assert child.is_or_is_descendant_of?(root)
|
||||
child.update_attribute :organization_id, 'different'
|
||||
assert !child.is_or_is_descendant_of?(root)
|
||||
end
|
||||
|
||||
def test_same_scope?
|
||||
root = Scoped.root
|
||||
child = root.children.first
|
||||
assert child.same_scope?(root)
|
||||
child.update_attribute :organization_id, 'different'
|
||||
assert !child.same_scope?(root)
|
||||
end
|
||||
|
||||
def test_left_sibling
|
||||
assert_equal categories(:child_1), categories(:child_2).left_sibling
|
||||
assert_equal categories(:child_2), categories(:child_3).left_sibling
|
||||
end
|
||||
|
||||
def test_left_sibling_of_root
|
||||
assert_nil categories(:top_level).left_sibling
|
||||
end
|
||||
|
||||
def test_left_sibling_without_siblings
|
||||
assert_nil categories(:child_2_1).left_sibling
|
||||
end
|
||||
|
||||
def test_left_sibling_of_leftmost_node
|
||||
assert_nil categories(:child_1).left_sibling
|
||||
end
|
||||
|
||||
def test_right_sibling
|
||||
assert_equal categories(:child_3), categories(:child_2).right_sibling
|
||||
assert_equal categories(:child_2), categories(:child_1).right_sibling
|
||||
end
|
||||
|
||||
def test_right_sibling_of_root
|
||||
assert_equal categories(:top_level_2), categories(:top_level).right_sibling
|
||||
assert_nil categories(:top_level_2).right_sibling
|
||||
end
|
||||
|
||||
def test_right_sibling_without_siblings
|
||||
assert_nil categories(:child_2_1).right_sibling
|
||||
end
|
||||
|
||||
def test_right_sibling_of_rightmost_node
|
||||
assert_nil categories(:child_3).right_sibling
|
||||
end
|
||||
|
||||
def test_move_left
|
||||
categories(:child_2).move_left
|
||||
assert_nil categories(:child_2).left_sibling
|
||||
assert_equal categories(:child_1), categories(:child_2).right_sibling
|
||||
assert Category.valid?
|
||||
end
|
||||
|
||||
def test_move_right
|
||||
categories(:child_2).move_right
|
||||
assert_nil categories(:child_2).right_sibling
|
||||
assert_equal categories(:child_3), categories(:child_2).left_sibling
|
||||
assert Category.valid?
|
||||
end
|
||||
|
||||
def test_move_to_left_of
|
||||
categories(:child_3).move_to_left_of(categories(:child_1))
|
||||
assert_nil categories(:child_3).left_sibling
|
||||
assert_equal categories(:child_1), categories(:child_3).right_sibling
|
||||
assert Category.valid?
|
||||
end
|
||||
|
||||
def test_move_to_right_of
|
||||
categories(:child_1).move_to_right_of(categories(:child_3))
|
||||
assert_nil categories(:child_1).right_sibling
|
||||
assert_equal categories(:child_3), categories(:child_1).left_sibling
|
||||
assert Category.valid?
|
||||
end
|
||||
|
||||
def test_move_to_root
|
||||
categories(:child_2).move_to_root
|
||||
assert_nil categories(:child_2).parent
|
||||
assert_equal 0, categories(:child_2).level
|
||||
assert_equal 1, categories(:child_2_1).level
|
||||
assert_equal 1, categories(:child_2).left
|
||||
assert_equal 4, categories(:child_2).right
|
||||
assert Category.valid?
|
||||
end
|
||||
|
||||
def test_move_to_child_of
|
||||
categories(:child_1).move_to_child_of(categories(:child_3))
|
||||
assert_equal categories(:child_3).id, categories(:child_1).parent_id
|
||||
assert Category.valid?
|
||||
end
|
||||
|
||||
def test_move_to_child_of_appends_to_end
|
||||
child = Category.create! :name => 'New Child'
|
||||
child.move_to_child_of categories(:top_level)
|
||||
assert_equal child, categories(:top_level).children.last
|
||||
end
|
||||
|
||||
def test_subtree_move_to_child_of
|
||||
assert_equal 4, categories(:child_2).left
|
||||
assert_equal 7, categories(:child_2).right
|
||||
|
||||
assert_equal 2, categories(:child_1).left
|
||||
assert_equal 3, categories(:child_1).right
|
||||
|
||||
categories(:child_2).move_to_child_of(categories(:child_1))
|
||||
assert Category.valid?
|
||||
assert_equal categories(:child_1).id, categories(:child_2).parent_id
|
||||
|
||||
assert_equal 3, categories(:child_2).left
|
||||
assert_equal 6, categories(:child_2).right
|
||||
assert_equal 2, categories(:child_1).left
|
||||
assert_equal 7, categories(:child_1).right
|
||||
end
|
||||
|
||||
def test_slightly_difficult_move_to_child_of
|
||||
assert_equal 11, categories(:top_level_2).left
|
||||
assert_equal 12, categories(:top_level_2).right
|
||||
|
||||
# create a new top-level node and move single-node top-level tree inside it.
|
||||
new_top = Category.create(:name => 'New Top')
|
||||
assert_equal 13, new_top.left
|
||||
assert_equal 14, new_top.right
|
||||
|
||||
categories(:top_level_2).move_to_child_of(new_top)
|
||||
|
||||
assert Category.valid?
|
||||
assert_equal new_top.id, categories(:top_level_2).parent_id
|
||||
|
||||
assert_equal 12, categories(:top_level_2).left
|
||||
assert_equal 13, categories(:top_level_2).right
|
||||
assert_equal 11, new_top.left
|
||||
assert_equal 14, new_top.right
|
||||
end
|
||||
|
||||
def test_difficult_move_to_child_of
|
||||
assert_equal 1, categories(:top_level).left
|
||||
assert_equal 10, categories(:top_level).right
|
||||
assert_equal 5, categories(:child_2_1).left
|
||||
assert_equal 6, categories(:child_2_1).right
|
||||
|
||||
# create a new top-level node and move an entire top-level tree inside it.
|
||||
new_top = Category.create(:name => 'New Top')
|
||||
categories(:top_level).move_to_child_of(new_top)
|
||||
categories(:child_2_1).reload
|
||||
assert Category.valid?
|
||||
assert_equal new_top.id, categories(:top_level).parent_id
|
||||
|
||||
assert_equal 4, categories(:top_level).left
|
||||
assert_equal 13, categories(:top_level).right
|
||||
assert_equal 8, categories(:child_2_1).left
|
||||
assert_equal 9, categories(:child_2_1).right
|
||||
end
|
||||
|
||||
#rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
|
||||
def test_move_to_child_more_than_once_per_parent_rebuild
|
||||
root1 = Category.create(:name => 'Root1')
|
||||
root2 = Category.create(:name => 'Root2')
|
||||
root3 = Category.create(:name => 'Root3')
|
||||
|
||||
root2.move_to_child_of root1
|
||||
root3.move_to_child_of root1
|
||||
|
||||
output = Category.roots.last.to_text
|
||||
Category.update_all('lft = null, rgt = null')
|
||||
Category.rebuild!
|
||||
|
||||
assert_equal Category.roots.last.to_text, output
|
||||
end
|
||||
|
||||
# doing move_to_child twice onto same parent from the furthest right first
|
||||
def test_move_to_child_more_than_once_per_parent_outside_in
|
||||
node1 = Category.create(:name => 'Node-1')
|
||||
node2 = Category.create(:name => 'Node-2')
|
||||
node3 = Category.create(:name => 'Node-3')
|
||||
|
||||
node2.move_to_child_of node1
|
||||
node3.move_to_child_of node1
|
||||
|
||||
output = Category.roots.last.to_text
|
||||
Category.update_all('lft = null, rgt = null')
|
||||
Category.rebuild!
|
||||
|
||||
assert_equal Category.roots.last.to_text, output
|
||||
end
|
||||
|
||||
|
||||
def test_valid_with_null_lefts
|
||||
assert Category.valid?
|
||||
Category.update_all('lft = null')
|
||||
assert !Category.valid?
|
||||
end
|
||||
|
||||
def test_valid_with_null_rights
|
||||
assert Category.valid?
|
||||
Category.update_all('rgt = null')
|
||||
assert !Category.valid?
|
||||
end
|
||||
|
||||
def test_valid_with_missing_intermediate_node
|
||||
# Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
|
||||
assert Category.valid?
|
||||
Category.delete(categories(:child_2).id)
|
||||
assert Category.valid?
|
||||
end
|
||||
|
||||
def test_valid_with_overlapping_and_rights
|
||||
assert Category.valid?
|
||||
categories(:top_level_2)['lft'] = 0
|
||||
categories(:top_level_2).save
|
||||
assert !Category.valid?
|
||||
end
|
||||
|
||||
def test_rebuild
|
||||
assert Category.valid?
|
||||
before_text = Category.root.to_text
|
||||
Category.update_all('lft = null, rgt = null')
|
||||
Category.rebuild!
|
||||
assert Category.valid?
|
||||
assert_equal before_text, Category.root.to_text
|
||||
end
|
||||
|
||||
def test_move_possible_for_sibling
|
||||
assert categories(:child_2).move_possible?(categories(:child_1))
|
||||
end
|
||||
|
||||
def test_move_not_possible_to_self
|
||||
assert !categories(:top_level).move_possible?(categories(:top_level))
|
||||
end
|
||||
|
||||
def test_move_not_possible_to_parent
|
||||
categories(:top_level).descendants.each do |descendant|
|
||||
assert !categories(:top_level).move_possible?(descendant)
|
||||
assert descendant.move_possible?(categories(:top_level))
|
||||
end
|
||||
end
|
||||
|
||||
def test_is_or_is_ancestor_of?
|
||||
[:child_1, :child_2, :child_2_1, :child_3].each do |c|
|
||||
assert categories(:top_level).is_or_is_ancestor_of?(categories(c))
|
||||
end
|
||||
assert !categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2))
|
||||
end
|
||||
|
||||
def test_left_and_rights_valid_with_blank_left
|
||||
assert Category.left_and_rights_valid?
|
||||
categories(:child_2)[:lft] = nil
|
||||
categories(:child_2).save(false)
|
||||
assert !Category.left_and_rights_valid?
|
||||
end
|
||||
|
||||
def test_left_and_rights_valid_with_blank_right
|
||||
assert Category.left_and_rights_valid?
|
||||
categories(:child_2)[:rgt] = nil
|
||||
categories(:child_2).save(false)
|
||||
assert !Category.left_and_rights_valid?
|
||||
end
|
||||
|
||||
def test_left_and_rights_valid_with_equal
|
||||
assert Category.left_and_rights_valid?
|
||||
categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
|
||||
categories(:top_level_2).save(false)
|
||||
assert !Category.left_and_rights_valid?
|
||||
end
|
||||
|
||||
def test_left_and_rights_valid_with_left_equal_to_parent
|
||||
assert Category.left_and_rights_valid?
|
||||
categories(:child_2)[:lft] = categories(:top_level)[:lft]
|
||||
categories(:child_2).save(false)
|
||||
assert !Category.left_and_rights_valid?
|
||||
end
|
||||
|
||||
def test_left_and_rights_valid_with_right_equal_to_parent
|
||||
assert Category.left_and_rights_valid?
|
||||
categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
|
||||
categories(:child_2).save(false)
|
||||
assert !Category.left_and_rights_valid?
|
||||
end
|
||||
|
||||
def test_moving_dirty_objects_doesnt_invalidate_tree
|
||||
r1 = Category.create
|
||||
r2 = Category.create
|
||||
r3 = Category.create
|
||||
r4 = Category.create
|
||||
nodes = [r1, r2, r3, r4]
|
||||
|
||||
r2.move_to_child_of(r1)
|
||||
assert Category.valid?
|
||||
|
||||
r3.move_to_child_of(r1)
|
||||
assert Category.valid?
|
||||
|
||||
r4.move_to_child_of(r2)
|
||||
assert Category.valid?
|
||||
end
|
||||
|
||||
def test_multi_scoped_no_duplicates_for_columns?
|
||||
assert_nothing_raised do
|
||||
Note.no_duplicates_for_columns?
|
||||
end
|
||||
end
|
||||
|
||||
def test_multi_scoped_all_roots_valid?
|
||||
assert_nothing_raised do
|
||||
Note.all_roots_valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_multi_scoped
|
||||
note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
|
||||
note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
|
||||
note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
|
||||
|
||||
assert_equal [note1, note2], note1.self_and_siblings
|
||||
assert_equal [note3], note3.self_and_siblings
|
||||
end
|
||||
|
||||
def test_multi_scoped_rebuild
|
||||
root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
|
||||
child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
|
||||
child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
|
||||
|
||||
child1.move_to_child_of root
|
||||
child2.move_to_child_of root
|
||||
|
||||
Note.update_all('lft = null, rgt = null')
|
||||
Note.rebuild!
|
||||
|
||||
assert_equal Note.roots.find_by_body('A'), root
|
||||
assert_equal [child1, child2], Note.roots.find_by_body('A').children
|
||||
end
|
||||
|
||||
def test_same_scope_with_multi_scopes
|
||||
assert_nothing_raised do
|
||||
notes(:scope1).same_scope?(notes(:child_1))
|
||||
end
|
||||
assert notes(:scope1).same_scope?(notes(:child_1))
|
||||
assert notes(:child_1).same_scope?(notes(:scope1))
|
||||
assert !notes(:scope1).same_scope?(notes(:scope2))
|
||||
end
|
||||
|
||||
def test_quoting_of_multi_scope_column_names
|
||||
assert_equal ["\"notable_id\"", "\"notable_type\""], Note.quoted_scope_column_names
|
||||
end
|
||||
|
||||
def test_equal_in_same_scope
|
||||
assert_equal notes(:scope1), notes(:scope1)
|
||||
assert_not_equal notes(:scope1), notes(:child_1)
|
||||
end
|
||||
|
||||
def test_equal_in_different_scopes
|
||||
assert_not_equal notes(:scope1), notes(:scope2)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
sqlite3:
|
||||
adapter: sqlite3
|
||||
dbfile: awesome_nested_set.sqlite3.db
|
||||
sqlite3mem:
|
||||
:adapter: sqlite3
|
||||
:dbfile: ":memory:"
|
||||
postgresql:
|
||||
:adapter: postgresql
|
||||
:username: postgres
|
||||
:password: postgres
|
||||
:database: awesome_nested_set_plugin_test
|
||||
:min_messages: ERROR
|
||||
mysql:
|
||||
:adapter: mysql
|
||||
:host: localhost
|
||||
:username: root
|
||||
:password:
|
||||
:database: awesome_nested_set_plugin_test
|
||||
@@ -0,0 +1,23 @@
|
||||
ActiveRecord::Schema.define(:version => 0) do
|
||||
|
||||
create_table :categories, :force => true do |t|
|
||||
t.column :name, :string
|
||||
t.column :parent_id, :integer
|
||||
t.column :lft, :integer
|
||||
t.column :rgt, :integer
|
||||
t.column :organization_id, :integer
|
||||
end
|
||||
|
||||
create_table :departments, :force => true do |t|
|
||||
t.column :name, :string
|
||||
end
|
||||
|
||||
create_table :notes, :force => true do |t|
|
||||
t.column :body, :text
|
||||
t.column :parent_id, :integer
|
||||
t.column :lft, :integer
|
||||
t.column :rgt, :integer
|
||||
t.column :notable_id, :integer
|
||||
t.column :notable_type, :string
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
top_level:
|
||||
id: 1
|
||||
name: Top Level
|
||||
lft: 1
|
||||
rgt: 10
|
||||
child_1:
|
||||
id: 2
|
||||
name: Child 1
|
||||
parent_id: 1
|
||||
lft: 2
|
||||
rgt: 3
|
||||
child_2:
|
||||
id: 3
|
||||
name: Child 2
|
||||
parent_id: 1
|
||||
lft: 4
|
||||
rgt: 7
|
||||
child_2_1:
|
||||
id: 4
|
||||
name: Child 2.1
|
||||
parent_id: 3
|
||||
lft: 5
|
||||
rgt: 6
|
||||
child_3:
|
||||
id: 5
|
||||
name: Child 3
|
||||
parent_id: 1
|
||||
lft: 8
|
||||
rgt: 9
|
||||
top_level_2:
|
||||
id: 6
|
||||
name: Top Level 2
|
||||
lft: 11
|
||||
rgt: 12
|
||||
@@ -0,0 +1,15 @@
|
||||
class Category < ActiveRecord::Base
|
||||
acts_as_nested_set
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def recurse &block
|
||||
block.call self, lambda{
|
||||
self.children.each do |child|
|
||||
child.recurse &block
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
top:
|
||||
id: 1
|
||||
name: Top
|
||||
@@ -0,0 +1,38 @@
|
||||
scope1:
|
||||
id: 1
|
||||
body: Top Level
|
||||
lft: 1
|
||||
rgt: 10
|
||||
notable_id: 1
|
||||
notable_type: Category
|
||||
child_1:
|
||||
id: 2
|
||||
body: Child 1
|
||||
parent_id: 1
|
||||
lft: 2
|
||||
rgt: 3
|
||||
notable_id: 1
|
||||
notable_type: Category
|
||||
child_2:
|
||||
id: 3
|
||||
body: Child 2
|
||||
parent_id: 1
|
||||
lft: 4
|
||||
rgt: 7
|
||||
notable_id: 1
|
||||
notable_type: Category
|
||||
child_3:
|
||||
id: 4
|
||||
body: Child 3
|
||||
parent_id: 1
|
||||
lft: 8
|
||||
rgt: 9
|
||||
notable_id: 1
|
||||
notable_type: Category
|
||||
scope2:
|
||||
id: 5
|
||||
body: Top Level 2
|
||||
lft: 1
|
||||
rgt: 2
|
||||
notable_id: 1
|
||||
notable_type: Departments
|
||||
@@ -0,0 +1,31 @@
|
||||
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
||||
plugin_test_dir = File.dirname(__FILE__)
|
||||
|
||||
require 'rubygems'
|
||||
require 'test/unit'
|
||||
require 'multi_rails_init'
|
||||
# gem 'activerecord', '>= 2.0'
|
||||
require 'active_record'
|
||||
require 'action_controller'
|
||||
require 'action_view'
|
||||
require 'active_record/fixtures'
|
||||
|
||||
require plugin_test_dir + '/../init.rb'
|
||||
|
||||
ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
|
||||
|
||||
ActiveRecord::Base.configurations = YAML::load(IO.read(plugin_test_dir + "/db/database.yml"))
|
||||
ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite3mem")
|
||||
ActiveRecord::Migration.verbose = false
|
||||
load(File.join(plugin_test_dir, "db", "schema.rb"))
|
||||
|
||||
Dir["#{plugin_test_dir}/fixtures/*.rb"].each {|file| require file }
|
||||
|
||||
|
||||
class Test::Unit::TestCase #:nodoc:
|
||||
self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
|
||||
self.use_transactional_fixtures = true
|
||||
self.use_instantiated_fixtures = false
|
||||
|
||||
fixtures :categories, :notes, :departments
|
||||
end
|
||||
+2
-2
@@ -97,8 +97,8 @@ module ActionController
|
||||
"Unknown options: #{unknown_option_keys.join(', ')}" unless
|
||||
unknown_option_keys.empty?
|
||||
|
||||
options[:singular_name] ||= Inflector.singularize(collection_id.to_s)
|
||||
options[:class_name] ||= Inflector.camelize(options[:singular_name])
|
||||
options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
|
||||
options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name])
|
||||
end
|
||||
|
||||
# Returns a paginator and a collection of Active Record model instances
|
||||
|
||||
@@ -140,4 +140,4 @@ module Engines::RailsExtensions::Dependencies
|
||||
end
|
||||
end
|
||||
|
||||
Dependencies.send :include, Engines::RailsExtensions::Dependencies
|
||||
ActiveSupport::Dependencies.send :include, Engines::RailsExtensions::Dependencies
|
||||
+11
-5
@@ -52,7 +52,7 @@ module GLoc
|
||||
# Sets the current language for this instance/class.
|
||||
# Setting the language of a class effects all instances unless the instance has its own language defined.
|
||||
def set_language(language)
|
||||
@gloc_language= language.nil? ? nil : language.to_sym
|
||||
GLoc.current_language = language
|
||||
end
|
||||
|
||||
# Sets the current language if the language passed is a valid language.
|
||||
@@ -75,7 +75,7 @@ module GLoc
|
||||
include ::GLoc::InstanceMethods
|
||||
# Returns the instance-level current language, or if not set, returns the class-level current language.
|
||||
def current_language
|
||||
@gloc_language || self.class.current_language
|
||||
GLoc.current_language
|
||||
end
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
@@ -87,7 +87,7 @@ module GLoc
|
||||
include ::GLoc::InstanceMethods
|
||||
# Returns the current language, or if not set, returns the GLoc current language.
|
||||
def current_language
|
||||
@gloc_language || GLoc.current_language
|
||||
GLoc.current_language
|
||||
end
|
||||
end
|
||||
|
||||
@@ -103,10 +103,16 @@ module GLoc
|
||||
|
||||
class << self
|
||||
include ::GLoc::InstanceMethods
|
||||
|
||||
@@current_language = nil
|
||||
|
||||
# Returns the default language
|
||||
# Returns the current language
|
||||
def current_language
|
||||
GLoc::CONFIG[:default_language]
|
||||
@@current_language || GLoc::CONFIG[:default_language]
|
||||
end
|
||||
|
||||
def current_language=(lang)
|
||||
@@current_language = lang.blank? ? nil : lang.to_sym
|
||||
end
|
||||
|
||||
# Adds a collection of localized strings to the in-memory string store.
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ namespace :gloc do
|
||||
}
|
||||
end
|
||||
|
||||
desc 'Updates language files based on em.yml content'
|
||||
desc 'Updates language files based on en.yml content'
|
||||
task :update do
|
||||
dir = ENV['DIR'] || './lang'
|
||||
|
||||
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2007 West Arete Computing, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
== Gravatar Plugin
|
||||
|
||||
This plugin provides a handful of view helpers for displaying gravatars
|
||||
(globally-recognized avatars).
|
||||
|
||||
Gravatars allow users to configure an avatar to go with their email address at
|
||||
a central location: http://gravatar.com. Gravatar-aware websites (such
|
||||
as yours) can then look up and display each user's preferred avatar, without
|
||||
having to handle avatar management. The user gets the benefit of not having to
|
||||
set up an avatar for each site that they post on.
|
||||
|
||||
== Installation
|
||||
|
||||
cd ~/myapp
|
||||
ruby script/plugin install svn://rubyforge.org//var/svn/gravatarplugin/plugins/gravatar
|
||||
|
||||
or, if you're using piston[http://piston.rubyforge.org] (worth it!):
|
||||
|
||||
cd ~/myapp/vendor/plugins
|
||||
piston import svn://rubyforge.org//var/svn/gravatarplugin/plugins/gravatar
|
||||
|
||||
== Example
|
||||
|
||||
If you represent your users with a model that has an +email+ method (typical
|
||||
for most rails authentication setups), then you can simply use this method
|
||||
in your views:
|
||||
|
||||
<%= gravatar_for @user %>
|
||||
|
||||
This will be replaced with the full HTML +img+ tag necessary for displaying
|
||||
that user's gravatar.
|
||||
|
||||
Other helpers are documented under GravatarHelper::PublicMethods.
|
||||
|
||||
== Acknowledgments
|
||||
|
||||
The following people have also written gravatar-related Ruby libraries:
|
||||
* Seth Rasmussen created the gravatar gem[http://gravatar.rubyforge.org]
|
||||
* Matt McCray has also created a gravatar
|
||||
plugin[http://mattmccray.com/svn/rails/plugins/gravatar_helper]
|
||||
|
||||
== Author
|
||||
|
||||
Scott A. Woods
|
||||
West Arete Computing, Inc.
|
||||
http://westarete.com
|
||||
scott at westarete dot com
|
||||
|
||||
== TODO
|
||||
|
||||
* Get full spec coverage
|
||||
* Finish rdoc documentation
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
require 'spec/rake/spectask'
|
||||
require 'rake/rdoctask'
|
||||
|
||||
desc 'Default: run all specs'
|
||||
task :default => :spec
|
||||
|
||||
desc 'Run all application-specific specs'
|
||||
Spec::Rake::SpecTask.new(:spec) do |t|
|
||||
t.warning = true
|
||||
t.rcov = true
|
||||
end
|
||||
|
||||
desc "Report code statistics (KLOCs, etc) from the application"
|
||||
task :stats do
|
||||
RAILS_ROOT = File.dirname(__FILE__)
|
||||
STATS_DIRECTORIES = [
|
||||
%w(Libraries lib/),
|
||||
%w(Specs spec/),
|
||||
].collect { |name, dir| [ name, "#{RAILS_ROOT}/#{dir}" ] }.select { |name, dir| File.directory?(dir) }
|
||||
require 'code_statistics'
|
||||
CodeStatistics.new(*STATS_DIRECTORIES).to_s
|
||||
end
|
||||
|
||||
namespace :doc do
|
||||
desc 'Generate documentation for the assert_request plugin.'
|
||||
Rake::RDocTask.new(:plugin) do |rdoc|
|
||||
rdoc.rdoc_dir = 'rdoc'
|
||||
rdoc.title = 'Gravatar Rails Plugin'
|
||||
rdoc.options << '--line-numbers' << '--inline-source' << '--accessor' << 'cattr_accessor=rw'
|
||||
rdoc.rdoc_files.include('README')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
end
|
||||
end
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
author: Scott Woods, West Arete Computing
|
||||
summary: View helpers for displaying gravatars.
|
||||
homepage: http://gravatarplugin.rubyforge.org/
|
||||
plugin: svn://rubyforge.org//var/svn/gravatarplugin/plugins/gravatar
|
||||
license: MIT
|
||||
version: 0.1
|
||||
rails_version: 1.0+
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
require 'gravatar'
|
||||
ActionView::Base.send :include, GravatarHelper::PublicMethods
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
require 'digest/md5'
|
||||
require 'cgi'
|
||||
|
||||
module GravatarHelper
|
||||
|
||||
# These are the options that control the default behavior of the public
|
||||
# methods. They can be overridden during the actual call to the helper,
|
||||
# or you can set them in your environment.rb as such:
|
||||
#
|
||||
# # Allow racier gravatars
|
||||
# GravatarHelper::DEFAULT_OPTIONS[:rating] = 'R'
|
||||
#
|
||||
DEFAULT_OPTIONS = {
|
||||
# The URL of a default image to display if the given email address does
|
||||
# not have a gravatar.
|
||||
:default => nil,
|
||||
|
||||
# The default size in pixels for the gravatar image (they're square).
|
||||
:size => 50,
|
||||
|
||||
# The maximum allowed MPAA rating for gravatars. This allows you to
|
||||
# exclude gravatars that may be out of character for your site.
|
||||
:rating => 'PG',
|
||||
|
||||
# The alt text to use in the img tag for the gravatar.
|
||||
:alt => 'avatar',
|
||||
|
||||
# The class to assign to the img tag for the gravatar.
|
||||
:class => 'gravatar',
|
||||
}
|
||||
|
||||
# The methods that will be made available to your views.
|
||||
module PublicMethods
|
||||
|
||||
# Return the HTML img tag for the given user's gravatar. Presumes that
|
||||
# the given user object will respond_to "email", and return the user's
|
||||
# email address.
|
||||
def gravatar_for(user, options={})
|
||||
gravatar(user.email, options)
|
||||
end
|
||||
|
||||
# Return the HTML img tag for the given email address's gravatar.
|
||||
def gravatar(email, options={})
|
||||
src = h(gravatar_url(email, options))
|
||||
options = DEFAULT_OPTIONS.merge(options)
|
||||
[:class, :alt, :size].each { |opt| options[opt] = h(options[opt]) }
|
||||
"<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />"
|
||||
end
|
||||
|
||||
# Return the gravatar URL for the given email address.
|
||||
def gravatar_url(email, options={})
|
||||
email_hash = Digest::MD5.hexdigest(email)
|
||||
options = DEFAULT_OPTIONS.merge(options)
|
||||
options[:default] = CGI::escape(options[:default]) unless options[:default].nil?
|
||||
returning "http://www.gravatar.com/avatar.php?gravatar_id=#{email_hash}" do |url|
|
||||
[:rating, :size, :default].each do |opt|
|
||||
unless options[opt].nil?
|
||||
value = h(options[opt])
|
||||
url << "&#{opt}=#{value}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
require 'rubygems'
|
||||
require 'erb' # to get "h"
|
||||
require 'active_support' # to get "returning"
|
||||
require File.dirname(__FILE__) + '/../lib/gravatar'
|
||||
include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util
|
||||
|
||||
context "gravatar_url with a custom default URL" do
|
||||
setup do
|
||||
@original_options = DEFAULT_OPTIONS.dup
|
||||
DEFAULT_OPTIONS[:default] = "no_avatar.png"
|
||||
@url = gravatar_url("somewhere")
|
||||
end
|
||||
|
||||
specify "should include the \"default\" argument in the result" do
|
||||
@url.should match(/&default=no_avatar.png/)
|
||||
end
|
||||
|
||||
teardown do
|
||||
DEFAULT_OPTIONS.merge!(@original_options)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "gravatar_url with default settings" do
|
||||
setup do
|
||||
@url = gravatar_url("somewhere")
|
||||
end
|
||||
|
||||
specify "should have a nil default URL" do
|
||||
DEFAULT_OPTIONS[:default].should be_nil
|
||||
end
|
||||
|
||||
specify "should not include the \"default\" argument in the result" do
|
||||
@url.should_not match(/&default=/)
|
||||
end
|
||||
|
||||
end
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
* Fake HTTP method from OpenID server since they only support a GET. Eliminates the need to set an extra route to match the server's reply. [Josh Peek]
|
||||
|
||||
* OpenID 2.0 recommends that forms should use the field name "openid_identifier" rather than "openid_url" [Josh Peek]
|
||||
|
||||
* Return open_id_response.display_identifier to the application instead of .endpoints.claimed_id. [nbibler]
|
||||
|
||||
* Add Timeout protection [Rick]
|
||||
|
||||
* An invalid identity url passed through authenticate_with_open_id will no longer raise an InvalidOpenId exception. Instead it will return Result[:missing] to the completion block.
|
||||
|
||||
* Allow a return_to option to be used instead of the requested url [Josh Peek]
|
||||
|
||||
* Updated plugin to use Ruby OpenID 2.x.x [Josh Peek]
|
||||
|
||||
* Tied plugin to ruby-openid 1.1.4 gem until we can make it compatible with 2.x [DHH]
|
||||
|
||||
* Use URI instead of regexps to normalize the URL and gain free, better matching #8136 [dkubb]
|
||||
|
||||
* Allow -'s in #normalize_url [Rick]
|
||||
|
||||
* remove instance of mattr_accessor, it was breaking tests since they don't load ActiveSupport. Fix Timeout test [Rick]
|
||||
|
||||
* Throw a InvalidOpenId exception instead of just a RuntimeError when the URL can't be normalized [DHH]
|
||||
|
||||
* Just use the path for the return URL, so extra query parameters don't interfere [DHH]
|
||||
|
||||
* Added a new default database-backed store after experiencing trouble with the filestore on NFS. The file store is still available as an option [DHH]
|
||||
|
||||
* Added normalize_url and applied it to all operations going through the plugin [DHH]
|
||||
|
||||
* Removed open_id? as the idea of using the same input box for both OpenID and username has died -- use using_open_id? instead (which checks for the presence of params[:openid_url] by default) [DHH]
|
||||
|
||||
* Added OpenIdAuthentication::Result to make it easier to deal with default situations where you don't care to do something particular for each error state [DHH]
|
||||
|
||||
* Stop relying on root_url being defined, we can just grab the current url instead [DHH]
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
OpenIdAuthentication
|
||||
====================
|
||||
|
||||
Provides a thin wrapper around the excellent ruby-openid gem from JanRan. Be sure to install that first:
|
||||
|
||||
gem install ruby-openid
|
||||
|
||||
To understand what OpenID is about and how it works, it helps to read the documentation for lib/openid/consumer.rb
|
||||
from that gem.
|
||||
|
||||
The specification used is http://openid.net/specs/openid-authentication-2_0.html.
|
||||
|
||||
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
OpenID authentication uses the session, so be sure that you haven't turned that off. It also relies on a number of
|
||||
database tables to store the authentication keys. So you'll have to run the migration to create these before you get started:
|
||||
|
||||
rake open_id_authentication:db:create
|
||||
|
||||
Or, use the included generators to install or upgrade:
|
||||
|
||||
./script/generate open_id_authentication_tables MigrationName
|
||||
./script/generate upgrade_open_id_authentication_tables MigrationName
|
||||
|
||||
Alternatively, you can use the file-based store, which just relies on on tmp/openids being present in RAILS_ROOT. But be aware that this store only works if you have a single application server. And it's not safe to use across NFS. It's recommended that you use the database store if at all possible. To use the file-based store, you'll also have to add this line to your config/environment.rb:
|
||||
|
||||
OpenIdAuthentication.store = :file
|
||||
|
||||
This particular plugin also relies on the fact that the authentication action allows for both POST and GET operations.
|
||||
If you're using RESTful authentication, you'll need to explicitly allow for this in your routes.rb.
|
||||
|
||||
The plugin also expects to find a root_url method that points to the home page of your site. You can accomplish this by using a root route in config/routes.rb:
|
||||
|
||||
map.root :controller => 'articles'
|
||||
|
||||
This plugin relies on Rails Edge revision 6317 or newer.
|
||||
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
This example is just to meant to demonstrate how you could use OpenID authentication. You might well want to add
|
||||
salted hash logins instead of plain text passwords and other requirements on top of this. Treat it as a starting point,
|
||||
not a destination.
|
||||
|
||||
Note that the User model referenced in the simple example below has an 'identity_url' attribute. You will want to add the same or similar field to whatever
|
||||
model you are using for authentication.
|
||||
|
||||
Also of note is the following code block used in the example below:
|
||||
|
||||
authenticate_with_open_id do |result, identity_url|
|
||||
...
|
||||
end
|
||||
|
||||
In the above code block, 'identity_url' will need to match user.identity_url exactly. 'identity_url' will be a string in the form of 'http://example.com' -
|
||||
If you are storing just 'example.com' with your user, the lookup will fail.
|
||||
|
||||
There is a handy method in this plugin called 'normalize_url' that will help with validating OpenID URLs.
|
||||
|
||||
OpenIdAuthentication.normalize_url(user.identity_url)
|
||||
|
||||
The above will return a standardized version of the OpenID URL - the above called with 'example.com' will return 'http://example.com/'
|
||||
It will also raise an InvalidOpenId exception if the URL is determined to not be valid.
|
||||
Use the above code in your User model and validate OpenID URLs before saving them.
|
||||
|
||||
config/routes.rb
|
||||
|
||||
map.root :controller => 'articles'
|
||||
map.resource :session
|
||||
|
||||
|
||||
app/views/sessions/new.erb
|
||||
|
||||
<% form_tag(session_url) do %>
|
||||
<p>
|
||||
<label for="name">Username:</label>
|
||||
<%= text_field_tag "name" %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="password">Password:</label>
|
||||
<%= password_field_tag %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
...or use:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="openid_identifier">OpenID:</label>
|
||||
<%= text_field_tag "openid_identifier" %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= submit_tag 'Sign in', :disable_with => "Signing in…" %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
app/controllers/sessions_controller.rb
|
||||
class SessionsController < ApplicationController
|
||||
def create
|
||||
if using_open_id?
|
||||
open_id_authentication
|
||||
else
|
||||
password_authentication(params[:name], params[:password])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
def password_authentication(name, password)
|
||||
if @current_user = @account.users.authenticate(params[:name], params[:password])
|
||||
successful_login
|
||||
else
|
||||
failed_login "Sorry, that username/password doesn't work"
|
||||
end
|
||||
end
|
||||
|
||||
def open_id_authentication
|
||||
authenticate_with_open_id do |result, identity_url|
|
||||
if result.successful?
|
||||
if @current_user = @account.users.find_by_identity_url(identity_url)
|
||||
successful_login
|
||||
else
|
||||
failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
|
||||
end
|
||||
else
|
||||
failed_login result.message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def successful_login
|
||||
session[:user_id] = @current_user.id
|
||||
redirect_to(root_url)
|
||||
end
|
||||
|
||||
def failed_login(message)
|
||||
flash[:error] = message
|
||||
redirect_to(new_session_url)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
If you're fine with the result messages above and don't need individual logic on a per-failure basis,
|
||||
you can collapse the case into a mere boolean:
|
||||
|
||||
def open_id_authentication
|
||||
authenticate_with_open_id do |result, identity_url|
|
||||
if result.successful? && @current_user = @account.users.find_by_identity_url(identity_url)
|
||||
successful_login
|
||||
else
|
||||
failed_login(result.message || "Sorry, no user by that identity URL exists (#{identity_url})")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Simple Registration OpenID Extension
|
||||
====================================
|
||||
|
||||
Some OpenID Providers support this lightweight profile exchange protocol. See more: http://www.openidenabled.com/openid/simple-registration-extension
|
||||
|
||||
You can support it in your app by changing #open_id_authentication
|
||||
|
||||
def open_id_authentication(identity_url)
|
||||
# Pass optional :required and :optional keys to specify what sreg fields you want.
|
||||
# Be sure to yield registration, a third argument in the #authenticate_with_open_id block.
|
||||
authenticate_with_open_id(identity_url,
|
||||
:required => [ :nickname, :email ],
|
||||
:optional => :fullname) do |result, identity_url, registration|
|
||||
case result.status
|
||||
when :missing
|
||||
failed_login "Sorry, the OpenID server couldn't be found"
|
||||
when :invalid
|
||||
failed_login "Sorry, but this does not appear to be a valid OpenID"
|
||||
when :canceled
|
||||
failed_login "OpenID verification was canceled"
|
||||
when :failed
|
||||
failed_login "Sorry, the OpenID verification failed"
|
||||
when :successful
|
||||
if @current_user = @account.users.find_by_identity_url(identity_url)
|
||||
assign_registration_attributes!(registration)
|
||||
|
||||
if current_user.save
|
||||
successful_login
|
||||
else
|
||||
failed_login "Your OpenID profile registration failed: " +
|
||||
@current_user.errors.full_messages.to_sentence
|
||||
end
|
||||
else
|
||||
failed_login "Sorry, no user by that identity URL exists"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# registration is a hash containing the valid sreg keys given above
|
||||
# use this to map them to fields of your user model
|
||||
def assign_registration_attributes!(registration)
|
||||
model_to_registration_mapping.each do |model_attribute, registration_attribute|
|
||||
unless registration[registration_attribute].blank?
|
||||
@current_user.send("#{model_attribute}=", registration[registration_attribute])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def model_to_registration_mapping
|
||||
{ :login => 'nickname', :email => 'email', :display_name => 'fullname' }
|
||||
end
|
||||
|
||||
Attribute Exchange OpenID Extension
|
||||
===================================
|
||||
|
||||
Some OpenID providers also support the OpenID AX (attribute exchange) protocol for exchanging identity information between endpoints. See more: http://openid.net/specs/openid-attribute-exchange-1_0.html
|
||||
|
||||
Accessing AX data is very similar to the Simple Registration process, described above -- just add the URI identifier for the AX field to your :optional or :required parameters. For example:
|
||||
|
||||
authenticate_with_open_id(identity_url,
|
||||
:required => [ :email, 'http://schema.openid.net/birthDate' ]) do |result, identity_url, registration|
|
||||
|
||||
This would provide the sreg data for :email, and the AX data for 'http://schema.openid.net/birthDate'
|
||||
|
||||
|
||||
|
||||
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
|
||||
desc 'Default: run unit tests.'
|
||||
task :default => :test
|
||||
|
||||
desc 'Test the open_id_authentication plugin.'
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
t.libs << 'lib'
|
||||
t.pattern = 'test/**/*_test.rb'
|
||||
t.verbose = true
|
||||
end
|
||||
|
||||
desc 'Generate documentation for the open_id_authentication plugin.'
|
||||
Rake::RDocTask.new(:rdoc) do |rdoc|
|
||||
rdoc.rdoc_dir = 'rdoc'
|
||||
rdoc.title = 'OpenIdAuthentication'
|
||||
rdoc.options << '--line-numbers' << '--inline-source'
|
||||
rdoc.rdoc_files.include('README')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
end
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
class OpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase
|
||||
def initialize(runtime_args, runtime_options = {})
|
||||
super
|
||||
end
|
||||
|
||||
def manifest
|
||||
record do |m|
|
||||
m.migration_template 'migration.rb', 'db/migrate'
|
||||
end
|
||||
end
|
||||
end
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
class <%= class_name %> < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :open_id_authentication_associations, :force => true do |t|
|
||||
t.integer :issued, :lifetime
|
||||
t.string :handle, :assoc_type
|
||||
t.binary :server_url, :secret
|
||||
end
|
||||
|
||||
create_table :open_id_authentication_nonces, :force => true do |t|
|
||||
t.integer :timestamp, :null => false
|
||||
t.string :server_url, :null => true
|
||||
t.string :salt, :null => false
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :open_id_authentication_associations
|
||||
drop_table :open_id_authentication_nonces
|
||||
end
|
||||
end
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
class <%= class_name %> < ActiveRecord::Migration
|
||||
def self.up
|
||||
drop_table :open_id_authentication_settings
|
||||
drop_table :open_id_authentication_nonces
|
||||
|
||||
create_table :open_id_authentication_nonces, :force => true do |t|
|
||||
t.integer :timestamp, :null => false
|
||||
t.string :server_url, :null => true
|
||||
t.string :salt, :null => false
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :open_id_authentication_nonces
|
||||
|
||||
create_table :open_id_authentication_nonces, :force => true do |t|
|
||||
t.integer :created
|
||||
t.string :nonce
|
||||
end
|
||||
|
||||
create_table :open_id_authentication_settings, :force => true do |t|
|
||||
t.string :setting
|
||||
t.binary :value
|
||||
end
|
||||
end
|
||||
end
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
class UpgradeOpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase
|
||||
def initialize(runtime_args, runtime_options = {})
|
||||
super
|
||||
end
|
||||
|
||||
def manifest
|
||||
record do |m|
|
||||
m.migration_template 'migration.rb', 'db/migrate'
|
||||
end
|
||||
end
|
||||
end
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
begin
|
||||
require 'openid'
|
||||
rescue LoadError
|
||||
begin
|
||||
gem 'ruby-openid', '>=2.1.4'
|
||||
rescue Gem::LoadError
|
||||
# no openid support
|
||||
end
|
||||
end
|
||||
|
||||
if Object.const_defined?(:OpenID)
|
||||
config.to_prepare do
|
||||
OpenID::Util.logger = Rails.logger
|
||||
ActionController::Base.send :include, OpenIdAuthentication
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,241 @@
|
||||
require 'uri'
|
||||
require 'openid/extensions/sreg'
|
||||
require 'openid/extensions/ax'
|
||||
require 'openid/store/filesystem'
|
||||
|
||||
require File.dirname(__FILE__) + '/open_id_authentication/db_store'
|
||||
require File.dirname(__FILE__) + '/open_id_authentication/mem_cache_store'
|
||||
require File.dirname(__FILE__) + '/open_id_authentication/request'
|
||||
require File.dirname(__FILE__) + '/open_id_authentication/timeout_fixes' if OpenID::VERSION == "2.0.4"
|
||||
|
||||
module OpenIdAuthentication
|
||||
OPEN_ID_AUTHENTICATION_DIR = RAILS_ROOT + "/tmp/openids"
|
||||
|
||||
def self.store
|
||||
@@store
|
||||
end
|
||||
|
||||
def self.store=(*store_option)
|
||||
store, *parameters = *([ store_option ].flatten)
|
||||
|
||||
@@store = case store
|
||||
when :db
|
||||
OpenIdAuthentication::DbStore.new
|
||||
when :mem_cache
|
||||
OpenIdAuthentication::MemCacheStore.new(*parameters)
|
||||
when :file
|
||||
OpenID::Store::Filesystem.new(OPEN_ID_AUTHENTICATION_DIR)
|
||||
else
|
||||
raise "Unknown store: #{store}"
|
||||
end
|
||||
end
|
||||
|
||||
self.store = :db
|
||||
|
||||
class InvalidOpenId < StandardError
|
||||
end
|
||||
|
||||
class Result
|
||||
ERROR_MESSAGES = {
|
||||
:missing => "Sorry, the OpenID server couldn't be found",
|
||||
:invalid => "Sorry, but this does not appear to be a valid OpenID",
|
||||
:canceled => "OpenID verification was canceled",
|
||||
:failed => "OpenID verification failed",
|
||||
:setup_needed => "OpenID verification needs setup"
|
||||
}
|
||||
|
||||
def self.[](code)
|
||||
new(code)
|
||||
end
|
||||
|
||||
def initialize(code)
|
||||
@code = code
|
||||
end
|
||||
|
||||
def status
|
||||
@code
|
||||
end
|
||||
|
||||
ERROR_MESSAGES.keys.each { |state| define_method("#{state}?") { @code == state } }
|
||||
|
||||
def successful?
|
||||
@code == :successful
|
||||
end
|
||||
|
||||
def unsuccessful?
|
||||
ERROR_MESSAGES.keys.include?(@code)
|
||||
end
|
||||
|
||||
def message
|
||||
ERROR_MESSAGES[@code]
|
||||
end
|
||||
end
|
||||
|
||||
# normalizes an OpenID according to http://openid.net/specs/openid-authentication-2_0.html#normalization
|
||||
def self.normalize_identifier(identifier)
|
||||
# clean up whitespace
|
||||
identifier = identifier.to_s.strip
|
||||
|
||||
# if an XRI has a prefix, strip it.
|
||||
identifier.gsub!(/xri:\/\//i, '')
|
||||
|
||||
# dodge XRIs -- TODO: validate, don't just skip.
|
||||
unless ['=', '@', '+', '$', '!', '('].include?(identifier.at(0))
|
||||
# does it begin with http? if not, add it.
|
||||
identifier = "http://#{identifier}" unless identifier =~ /^http/i
|
||||
|
||||
# strip any fragments
|
||||
identifier.gsub!(/\#(.*)$/, '')
|
||||
|
||||
begin
|
||||
uri = URI.parse(identifier)
|
||||
uri.scheme = uri.scheme.downcase # URI should do this
|
||||
identifier = uri.normalize.to_s
|
||||
rescue URI::InvalidURIError
|
||||
raise InvalidOpenId.new("#{identifier} is not an OpenID identifier")
|
||||
end
|
||||
end
|
||||
|
||||
return identifier
|
||||
end
|
||||
|
||||
# deprecated for OpenID 2.0, where not all OpenIDs are URLs
|
||||
def self.normalize_url(url)
|
||||
ActiveSupport::Deprecation.warn "normalize_url has been deprecated, use normalize_identifier instead"
|
||||
self.normalize_identifier(url)
|
||||
end
|
||||
|
||||
protected
|
||||
def normalize_url(url)
|
||||
OpenIdAuthentication.normalize_url(url)
|
||||
end
|
||||
|
||||
def normalize_identifier(url)
|
||||
OpenIdAuthentication.normalize_identifier(url)
|
||||
end
|
||||
|
||||
# The parameter name of "openid_identifier" is used rather than the Rails convention "open_id_identifier"
|
||||
# because that's what the specification dictates in order to get browser auto-complete working across sites
|
||||
def using_open_id?(identity_url = nil) #:doc:
|
||||
identity_url ||= params[:openid_identifier] || params[:openid_url]
|
||||
!identity_url.blank? || params[:open_id_complete]
|
||||
end
|
||||
|
||||
def authenticate_with_open_id(identity_url = nil, options = {}, &block) #:doc:
|
||||
identity_url ||= params[:openid_identifier] || params[:openid_url]
|
||||
|
||||
if params[:open_id_complete].nil?
|
||||
begin_open_id_authentication(identity_url, options, &block)
|
||||
else
|
||||
complete_open_id_authentication(&block)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def begin_open_id_authentication(identity_url, options = {})
|
||||
identity_url = normalize_identifier(identity_url)
|
||||
return_to = options.delete(:return_to)
|
||||
method = options.delete(:method)
|
||||
|
||||
options[:required] ||= [] # reduces validation later
|
||||
options[:optional] ||= []
|
||||
|
||||
open_id_request = open_id_consumer.begin(identity_url)
|
||||
add_simple_registration_fields(open_id_request, options)
|
||||
add_ax_fields(open_id_request, options)
|
||||
redirect_to(open_id_redirect_url(open_id_request, return_to, method))
|
||||
rescue OpenIdAuthentication::InvalidOpenId => e
|
||||
yield Result[:invalid], identity_url, nil
|
||||
rescue OpenID::OpenIDError, Timeout::Error => e
|
||||
logger.error("[OPENID] #{e}")
|
||||
yield Result[:missing], identity_url, nil
|
||||
end
|
||||
|
||||
def complete_open_id_authentication
|
||||
params_with_path = params.reject { |key, value| request.path_parameters[key] }
|
||||
params_with_path.delete(:format)
|
||||
open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params_with_path, requested_url) }
|
||||
identity_url = normalize_identifier(open_id_response.display_identifier) if open_id_response.display_identifier
|
||||
|
||||
case open_id_response.status
|
||||
when OpenID::Consumer::SUCCESS
|
||||
profile_data = {}
|
||||
|
||||
# merge the SReg data and the AX data into a single hash of profile data
|
||||
[ OpenID::SReg::Response, OpenID::AX::FetchResponse ].each do |data_response|
|
||||
if data_response.from_success_response( open_id_response )
|
||||
profile_data.merge! data_response.from_success_response( open_id_response ).data
|
||||
end
|
||||
end
|
||||
|
||||
yield Result[:successful], identity_url, profile_data
|
||||
when OpenID::Consumer::CANCEL
|
||||
yield Result[:canceled], identity_url, nil
|
||||
when OpenID::Consumer::FAILURE
|
||||
yield Result[:failed], identity_url, nil
|
||||
when OpenID::Consumer::SETUP_NEEDED
|
||||
yield Result[:setup_needed], open_id_response.setup_url, nil
|
||||
end
|
||||
end
|
||||
|
||||
def open_id_consumer
|
||||
OpenID::Consumer.new(session, OpenIdAuthentication.store)
|
||||
end
|
||||
|
||||
def add_simple_registration_fields(open_id_request, fields)
|
||||
sreg_request = OpenID::SReg::Request.new
|
||||
|
||||
# filter out AX identifiers (URIs)
|
||||
required_fields = fields[:required].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
|
||||
optional_fields = fields[:optional].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
|
||||
|
||||
sreg_request.request_fields(required_fields, true) unless required_fields.blank?
|
||||
sreg_request.request_fields(optional_fields, false) unless optional_fields.blank?
|
||||
sreg_request.policy_url = fields[:policy_url] if fields[:policy_url]
|
||||
open_id_request.add_extension(sreg_request)
|
||||
end
|
||||
|
||||
def add_ax_fields( open_id_request, fields )
|
||||
ax_request = OpenID::AX::FetchRequest.new
|
||||
|
||||
# look through the :required and :optional fields for URIs (AX identifiers)
|
||||
fields[:required].each do |f|
|
||||
next unless f =~ /^https?:\/\//
|
||||
ax_request.add( OpenID::AX::AttrInfo.new( f, nil, true ) )
|
||||
end
|
||||
|
||||
fields[:optional].each do |f|
|
||||
next unless f =~ /^https?:\/\//
|
||||
ax_request.add( OpenID::AX::AttrInfo.new( f, nil, false ) )
|
||||
end
|
||||
|
||||
open_id_request.add_extension( ax_request )
|
||||
end
|
||||
|
||||
def open_id_redirect_url(open_id_request, return_to = nil, method = nil)
|
||||
open_id_request.return_to_args['_method'] = (method || request.method).to_s
|
||||
open_id_request.return_to_args['open_id_complete'] = '1'
|
||||
open_id_request.redirect_url(root_url, return_to || requested_url)
|
||||
end
|
||||
|
||||
def requested_url
|
||||
relative_url_root = self.class.respond_to?(:relative_url_root) ?
|
||||
self.class.relative_url_root.to_s :
|
||||
request.relative_url_root
|
||||
"#{request.protocol}#{request.host_with_port}#{ActionController::AbstractRequest.relative_url_root}#{request.path}"
|
||||
end
|
||||
|
||||
def timeout_protection_from_identity_server
|
||||
yield
|
||||
rescue Timeout::Error
|
||||
Class.new do
|
||||
def status
|
||||
OpenID::FAILURE
|
||||
end
|
||||
|
||||
def msg
|
||||
"Identity server timed out"
|
||||
end
|
||||
end.new
|
||||
end
|
||||
end
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
module OpenIdAuthentication
|
||||
class Association < ActiveRecord::Base
|
||||
set_table_name :open_id_authentication_associations
|
||||
|
||||
def from_record
|
||||
OpenID::Association.new(handle, secret, issued, lifetime, assoc_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,55 @@
|
||||
require 'openid/store/interface'
|
||||
|
||||
module OpenIdAuthentication
|
||||
class DbStore < OpenID::Store::Interface
|
||||
def self.cleanup_nonces
|
||||
now = Time.now.to_i
|
||||
Nonce.delete_all(["timestamp > ? OR timestamp < ?", now + OpenID::Nonce.skew, now - OpenID::Nonce.skew])
|
||||
end
|
||||
|
||||
def self.cleanup_associations
|
||||
now = Time.now.to_i
|
||||
Association.delete_all(['issued + lifetime > ?',now])
|
||||
end
|
||||
|
||||
def store_association(server_url, assoc)
|
||||
remove_association(server_url, assoc.handle)
|
||||
Association.create(:server_url => server_url,
|
||||
:handle => assoc.handle,
|
||||
:secret => assoc.secret,
|
||||
:issued => assoc.issued,
|
||||
:lifetime => assoc.lifetime,
|
||||
:assoc_type => assoc.assoc_type)
|
||||
end
|
||||
|
||||
def get_association(server_url, handle = nil)
|
||||
assocs = if handle.blank?
|
||||
Association.find_all_by_server_url(server_url)
|
||||
else
|
||||
Association.find_all_by_server_url_and_handle(server_url, handle)
|
||||
end
|
||||
|
||||
assocs.reverse.each do |assoc|
|
||||
a = assoc.from_record
|
||||
if a.expires_in == 0
|
||||
assoc.destroy
|
||||
else
|
||||
return a
|
||||
end
|
||||
end if assocs.any?
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
def remove_association(server_url, handle)
|
||||
Association.delete_all(['server_url = ? AND handle = ?', server_url, handle]) > 0
|
||||
end
|
||||
|
||||
def use_nonce(server_url, timestamp, salt)
|
||||
return false if Nonce.find_by_server_url_and_timestamp_and_salt(server_url, timestamp, salt)
|
||||
return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
|
||||
Nonce.create(:server_url => server_url, :timestamp => timestamp, :salt => salt)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
require 'digest/sha1'
|
||||
require 'openid/store/interface'
|
||||
|
||||
module OpenIdAuthentication
|
||||
class MemCacheStore < OpenID::Store::Interface
|
||||
def initialize(*addresses)
|
||||
@connection = ActiveSupport::Cache::MemCacheStore.new(addresses)
|
||||
end
|
||||
|
||||
def store_association(server_url, assoc)
|
||||
server_key = association_server_key(server_url)
|
||||
assoc_key = association_key(server_url, assoc.handle)
|
||||
|
||||
assocs = @connection.read(server_key) || {}
|
||||
assocs[assoc.issued] = assoc_key
|
||||
|
||||
@connection.write(server_key, assocs)
|
||||
@connection.write(assoc_key, assoc, :expires_in => assoc.lifetime)
|
||||
end
|
||||
|
||||
def get_association(server_url, handle = nil)
|
||||
if handle
|
||||
@connection.read(association_key(server_url, handle))
|
||||
else
|
||||
server_key = association_server_key(server_url)
|
||||
assocs = @connection.read(server_key)
|
||||
return if assocs.nil?
|
||||
|
||||
last_key = assocs[assocs.keys.sort.last]
|
||||
@connection.read(last_key)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_association(server_url, handle)
|
||||
server_key = association_server_key(server_url)
|
||||
assoc_key = association_key(server_url, handle)
|
||||
assocs = @connection.read(server_key)
|
||||
|
||||
return false unless assocs && assocs.has_value?(assoc_key)
|
||||
|
||||
assocs = assocs.delete_if { |key, value| value == assoc_key }
|
||||
|
||||
@connection.write(server_key, assocs)
|
||||
@connection.delete(assoc_key)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def use_nonce(server_url, timestamp, salt)
|
||||
return false if @connection.read(nonce_key(server_url, salt))
|
||||
return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
|
||||
@connection.write(nonce_key(server_url, salt), timestamp, :expires_in => OpenID::Nonce.skew)
|
||||
return true
|
||||
end
|
||||
|
||||
private
|
||||
def association_key(server_url, handle = nil)
|
||||
"openid_association_#{digest(server_url)}_#{digest(handle)}"
|
||||
end
|
||||
|
||||
def association_server_key(server_url)
|
||||
"openid_association_server_#{digest(server_url)}"
|
||||
end
|
||||
|
||||
def nonce_key(server_url, salt)
|
||||
"openid_nonce_#{digest(server_url)}_#{digest(salt)}"
|
||||
end
|
||||
|
||||
def digest(text)
|
||||
Digest::SHA1.hexdigest(text)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
module OpenIdAuthentication
|
||||
class Nonce < ActiveRecord::Base
|
||||
set_table_name :open_id_authentication_nonces
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
module OpenIdAuthentication
|
||||
module Request
|
||||
def self.included(base)
|
||||
base.alias_method_chain :request_method, :openid
|
||||
end
|
||||
|
||||
def request_method_with_openid
|
||||
if !parameters[:_method].blank? && parameters[:open_id_complete] == '1'
|
||||
parameters[:_method].to_sym
|
||||
else
|
||||
request_method_without_openid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# In Rails 2.3, the request object has been renamed
|
||||
# from AbstractRequest to Request
|
||||
if defined? ActionController::Request
|
||||
ActionController::Request.send :include, OpenIdAuthentication::Request
|
||||
else
|
||||
ActionController::AbstractRequest.send :include, OpenIdAuthentication::Request
|
||||
end
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# http://trac.openidenabled.com/trac/ticket/156
|
||||
module OpenID
|
||||
@@timeout_threshold = 20
|
||||
|
||||
def self.timeout_threshold
|
||||
@@timeout_threshold
|
||||
end
|
||||
|
||||
def self.timeout_threshold=(value)
|
||||
@@timeout_threshold = value
|
||||
end
|
||||
|
||||
class StandardFetcher
|
||||
def make_http(uri)
|
||||
http = @proxy.new(uri.host, uri.port)
|
||||
http.read_timeout = http.open_timeout = OpenID.timeout_threshold
|
||||
http
|
||||
end
|
||||
end
|
||||
end
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
namespace :open_id_authentication do
|
||||
namespace :db do
|
||||
desc "Creates authentication tables for use with OpenIdAuthentication"
|
||||
task :create => :environment do
|
||||
generate_migration(["open_id_authentication_tables", "add_open_id_authentication_tables"])
|
||||
end
|
||||
|
||||
desc "Upgrade authentication tables from ruby-openid 1.x.x to 2.x.x"
|
||||
task :upgrade => :environment do
|
||||
generate_migration(["upgrade_open_id_authentication_tables", "upgrade_open_id_authentication_tables"])
|
||||
end
|
||||
|
||||
def generate_migration(args)
|
||||
require 'rails_generator'
|
||||
require 'rails_generator/scripts/generate'
|
||||
|
||||
if ActiveRecord::Base.connection.supports_migrations?
|
||||
Rails::Generator::Scripts::Generate.new.run(args)
|
||||
else
|
||||
raise "Task unavailable to this database (no migration support)"
|
||||
end
|
||||
end
|
||||
|
||||
desc "Clear the authentication tables"
|
||||
task :clear => :environment do
|
||||
OpenIdAuthentication::DbStore.cleanup_nonces
|
||||
OpenIdAuthentication::DbStore.cleanup_associations
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,151 @@
|
||||
require File.dirname(__FILE__) + '/test_helper'
|
||||
require File.dirname(__FILE__) + '/../lib/open_id_authentication/mem_cache_store'
|
||||
|
||||
# Mock MemCacheStore with MemoryStore for testing
|
||||
class OpenIdAuthentication::MemCacheStore < OpenID::Store::Interface
|
||||
def initialize(*addresses)
|
||||
@connection = ActiveSupport::Cache::MemoryStore.new
|
||||
end
|
||||
end
|
||||
|
||||
class MemCacheStoreTest < Test::Unit::TestCase
|
||||
ALLOWED_HANDLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
|
||||
|
||||
def setup
|
||||
@store = OpenIdAuthentication::MemCacheStore.new
|
||||
end
|
||||
|
||||
def test_store
|
||||
server_url = "http://www.myopenid.com/openid"
|
||||
assoc = gen_assoc(0)
|
||||
|
||||
# Make sure that a missing association returns no result
|
||||
assert_retrieve(server_url)
|
||||
|
||||
# Check that after storage, getting returns the same result
|
||||
@store.store_association(server_url, assoc)
|
||||
assert_retrieve(server_url, nil, assoc)
|
||||
|
||||
# more than once
|
||||
assert_retrieve(server_url, nil, assoc)
|
||||
|
||||
# Storing more than once has no ill effect
|
||||
@store.store_association(server_url, assoc)
|
||||
assert_retrieve(server_url, nil, assoc)
|
||||
|
||||
# Removing an association that does not exist returns not present
|
||||
assert_remove(server_url, assoc.handle + 'x', false)
|
||||
|
||||
# Removing an association that does not exist returns not present
|
||||
assert_remove(server_url + 'x', assoc.handle, false)
|
||||
|
||||
# Removing an association that is present returns present
|
||||
assert_remove(server_url, assoc.handle, true)
|
||||
|
||||
# but not present on subsequent calls
|
||||
assert_remove(server_url, assoc.handle, false)
|
||||
|
||||
# Put assoc back in the store
|
||||
@store.store_association(server_url, assoc)
|
||||
|
||||
# More recent and expires after assoc
|
||||
assoc2 = gen_assoc(1)
|
||||
@store.store_association(server_url, assoc2)
|
||||
|
||||
# After storing an association with a different handle, but the
|
||||
# same server_url, the handle with the later expiration is returned.
|
||||
assert_retrieve(server_url, nil, assoc2)
|
||||
|
||||
# We can still retrieve the older association
|
||||
assert_retrieve(server_url, assoc.handle, assoc)
|
||||
|
||||
# Plus we can retrieve the association with the later expiration
|
||||
# explicitly
|
||||
assert_retrieve(server_url, assoc2.handle, assoc2)
|
||||
|
||||
# More recent, and expires earlier than assoc2 or assoc. Make sure
|
||||
# that we're picking the one with the latest issued date and not
|
||||
# taking into account the expiration.
|
||||
assoc3 = gen_assoc(2, 100)
|
||||
@store.store_association(server_url, assoc3)
|
||||
|
||||
assert_retrieve(server_url, nil, assoc3)
|
||||
assert_retrieve(server_url, assoc.handle, assoc)
|
||||
assert_retrieve(server_url, assoc2.handle, assoc2)
|
||||
assert_retrieve(server_url, assoc3.handle, assoc3)
|
||||
|
||||
assert_remove(server_url, assoc2.handle, true)
|
||||
|
||||
assert_retrieve(server_url, nil, assoc3)
|
||||
assert_retrieve(server_url, assoc.handle, assoc)
|
||||
assert_retrieve(server_url, assoc2.handle, nil)
|
||||
assert_retrieve(server_url, assoc3.handle, assoc3)
|
||||
|
||||
assert_remove(server_url, assoc2.handle, false)
|
||||
assert_remove(server_url, assoc3.handle, true)
|
||||
|
||||
assert_retrieve(server_url, nil, assoc)
|
||||
assert_retrieve(server_url, assoc.handle, assoc)
|
||||
assert_retrieve(server_url, assoc2.handle, nil)
|
||||
assert_retrieve(server_url, assoc3.handle, nil)
|
||||
|
||||
assert_remove(server_url, assoc2.handle, false)
|
||||
assert_remove(server_url, assoc.handle, true)
|
||||
assert_remove(server_url, assoc3.handle, false)
|
||||
|
||||
assert_retrieve(server_url, nil, nil)
|
||||
assert_retrieve(server_url, assoc.handle, nil)
|
||||
assert_retrieve(server_url, assoc2.handle, nil)
|
||||
assert_retrieve(server_url, assoc3.handle, nil)
|
||||
|
||||
assert_remove(server_url, assoc2.handle, false)
|
||||
assert_remove(server_url, assoc.handle, false)
|
||||
assert_remove(server_url, assoc3.handle, false)
|
||||
end
|
||||
|
||||
def test_nonce
|
||||
server_url = "http://www.myopenid.com/openid"
|
||||
|
||||
[server_url, ''].each do |url|
|
||||
nonce1 = OpenID::Nonce::mk_nonce
|
||||
|
||||
assert_nonce(nonce1, true, url, "#{url}: nonce allowed by default")
|
||||
assert_nonce(nonce1, false, url, "#{url}: nonce not allowed twice")
|
||||
assert_nonce(nonce1, false, url, "#{url}: nonce not allowed third time")
|
||||
|
||||
# old nonces shouldn't pass
|
||||
old_nonce = OpenID::Nonce::mk_nonce(3600)
|
||||
assert_nonce(old_nonce, false, url, "Old nonce #{old_nonce.inspect} passed")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def gen_assoc(issued, lifetime = 600)
|
||||
secret = OpenID::CryptUtil.random_string(20, nil)
|
||||
handle = OpenID::CryptUtil.random_string(128, ALLOWED_HANDLE)
|
||||
OpenID::Association.new(handle, secret, Time.now + issued, lifetime, 'HMAC-SHA1')
|
||||
end
|
||||
|
||||
def assert_retrieve(url, handle = nil, expected = nil)
|
||||
assoc = @store.get_association(url, handle)
|
||||
|
||||
if expected.nil?
|
||||
assert_nil(assoc)
|
||||
else
|
||||
assert_equal(expected, assoc)
|
||||
assert_equal(expected.handle, assoc.handle)
|
||||
assert_equal(expected.secret, assoc.secret)
|
||||
end
|
||||
end
|
||||
|
||||
def assert_remove(url, handle, expected)
|
||||
present = @store.remove_association(url, handle)
|
||||
assert_equal(expected, present)
|
||||
end
|
||||
|
||||
def assert_nonce(nonce, expected, server_url, msg = "")
|
||||
stamp, salt = OpenID::Nonce::split_nonce(nonce)
|
||||
actual = @store.use_nonce(server_url, stamp, salt)
|
||||
assert_equal(expected, actual, msg)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
require File.dirname(__FILE__) + '/test_helper'
|
||||
|
||||
class NormalizeTest < Test::Unit::TestCase
|
||||
include OpenIdAuthentication
|
||||
|
||||
NORMALIZATIONS = {
|
||||
"openid.aol.com/nextangler" => "http://openid.aol.com/nextangler",
|
||||
"http://openid.aol.com/nextangler" => "http://openid.aol.com/nextangler",
|
||||
"https://openid.aol.com/nextangler" => "https://openid.aol.com/nextangler",
|
||||
"HTTP://OPENID.AOL.COM/NEXTANGLER" => "http://openid.aol.com/NEXTANGLER",
|
||||
"HTTPS://OPENID.AOL.COM/NEXTANGLER" => "https://openid.aol.com/NEXTANGLER",
|
||||
"loudthinking.com" => "http://loudthinking.com/",
|
||||
"http://loudthinking.com" => "http://loudthinking.com/",
|
||||
"http://loudthinking.com:80" => "http://loudthinking.com/",
|
||||
"https://loudthinking.com:443" => "https://loudthinking.com/",
|
||||
"http://loudthinking.com:8080" => "http://loudthinking.com:8080/",
|
||||
"techno-weenie.net" => "http://techno-weenie.net/",
|
||||
"http://techno-weenie.net" => "http://techno-weenie.net/",
|
||||
"http://techno-weenie.net " => "http://techno-weenie.net/",
|
||||
"=name" => "=name"
|
||||
}
|
||||
|
||||
def test_normalizations
|
||||
NORMALIZATIONS.each do |from, to|
|
||||
assert_equal to, normalize_identifier(from)
|
||||
end
|
||||
end
|
||||
|
||||
def test_broken_open_id
|
||||
assert_raises(InvalidOpenId) { normalize_identifier(nil) }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
require File.dirname(__FILE__) + '/test_helper'
|
||||
|
||||
class OpenIdAuthenticationTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@controller = Class.new do
|
||||
include OpenIdAuthentication
|
||||
def params() {} end
|
||||
end.new
|
||||
end
|
||||
|
||||
def test_authentication_should_fail_when_the_identity_server_is_missing
|
||||
open_id_consumer = mock()
|
||||
open_id_consumer.expects(:begin).raises(OpenID::OpenIDError)
|
||||
@controller.expects(:open_id_consumer).returns(open_id_consumer)
|
||||
@controller.expects(:logger).returns(mock(:error => true))
|
||||
|
||||
@controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url|
|
||||
assert result.missing?
|
||||
assert_equal "Sorry, the OpenID server couldn't be found", result.message
|
||||
end
|
||||
end
|
||||
|
||||
def test_authentication_should_be_invalid_when_the_identity_url_is_invalid
|
||||
@controller.send(:authenticate_with_open_id, "!") do |result, identity_url|
|
||||
assert result.invalid?, "Result expected to be invalid but was not"
|
||||
assert_equal "Sorry, but this does not appear to be a valid OpenID", result.message
|
||||
end
|
||||
end
|
||||
|
||||
def test_authentication_should_fail_when_the_identity_server_times_out
|
||||
open_id_consumer = mock()
|
||||
open_id_consumer.expects(:begin).raises(Timeout::Error, "Identity Server took too long.")
|
||||
@controller.expects(:open_id_consumer).returns(open_id_consumer)
|
||||
@controller.expects(:logger).returns(mock(:error => true))
|
||||
|
||||
@controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url|
|
||||
assert result.missing?
|
||||
assert_equal "Sorry, the OpenID server couldn't be found", result.message
|
||||
end
|
||||
end
|
||||
|
||||
def test_authentication_should_begin_when_the_identity_server_is_present
|
||||
@controller.expects(:begin_open_id_authentication)
|
||||
@controller.send(:authenticate_with_open_id, "http://someone.example.com")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
require File.dirname(__FILE__) + '/test_helper'
|
||||
|
||||
class StatusTest < Test::Unit::TestCase
|
||||
include OpenIdAuthentication
|
||||
|
||||
def test_state_conditional
|
||||
assert Result[:missing].missing?
|
||||
assert Result[:missing].unsuccessful?
|
||||
assert !Result[:missing].successful?
|
||||
|
||||
assert Result[:successful].successful?
|
||||
assert !Result[:successful].unsuccessful?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
require 'test/unit'
|
||||
require 'rubygems'
|
||||
|
||||
gem 'activesupport'
|
||||
require 'active_support'
|
||||
|
||||
gem 'actionpack'
|
||||
require 'action_controller'
|
||||
|
||||
gem 'mocha'
|
||||
require 'mocha'
|
||||
|
||||
gem 'ruby-openid'
|
||||
require 'openid'
|
||||
|
||||
RAILS_ROOT = File.dirname(__FILE__) unless defined? RAILS_ROOT
|
||||
require File.dirname(__FILE__) + "/../lib/open_id_authentication"
|
||||
Reference in new issue
Block a user