server.log
test.log
tmp
+public/engine_files
# Filters added to this controller will be run for all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
+require 'login_engine'
+
class ApplicationController < ActionController::Base
-end
\ No newline at end of file
+ include LoginEngine
+ helper :user
+ model :user
+end
class ElectionController < ApplicationController
model :raw_voter_list, :voter, :vote, :candidate
+ before_filter :login_required
+
## general methods for dealing with elections
####################################################################
def index
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
+ include LoginEngine
end
<head>
<title><%= @page_title || "VotingBooth" %></title>
<%= stylesheet_link_tag "vb", :media => "all" %>
+ <%= engine_stylesheet 'login_engine' %>
+
<%= javascript_include_tag "prototype", "effects", "dragdrop", "controls" %>
</head>
<body>
<td width="47%" valign="top">
<h2>Vote Administrators</h2>
-
+<%= render :controller => 'user', :action => 'login' %>
+hi!
</td>
</tr>
</table>
require 'randarray'
require 'rubyvote'
+module LoginEngine
+ config :salt, "voothingboat"
+end
+
+Engines.start :login
primary key (id)
);
+# CREATE users TABLE
+#####################################
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE `users` (
+ `id` int(11) NOT NULL auto_increment,
+ `login` varchar(80) NOT NULL default '',
+ `salted_password` varchar(40) NOT NULL default '',
+ `email` varchar(60) NOT NULL default '',
+ `firstname` varchar(40) default NULL,
+ `lastname` varchar(40) default NULL,
+ `salt` varchar(40) NOT NULL default '',
+ `verified` int(11) default '0',
+ `role` varchar(40) default NULL,
+ `security_token` varchar(40) default NULL,
+ `token_expiry` datetime default NULL,
+ `created_at` datetime default NULL,
+ `updated_at` datetime default NULL,
+ `logged_in_at` datetime default NULL,
+ `deleted` int(11) default '0',
+ `delete_after` datetime default NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
--- /dev/null
+# This file is autogenerated. Instead of editing this file, please use the
+# migrations feature of ActiveRecord to incrementally modify your database, and
+# then regenerate this schema definition.
+
+ActiveRecord::Schema.define(:version => 0) do
+
+ create_table "candidates", :force => true do |t|
+ t.column "election_id", :integer, :default => 0, :null => false
+ t.column "name", :string, :limit => 100, :default => "", :null => false
+ t.column "picture", :binary, :default => "", :null => false
+ end
+
+ create_table "elections", :force => true do |t|
+ t.column "name", :string, :limit => 100, :default => "", :null => false
+ t.column "description", :text, :default => "", :null => false
+ t.column "anonymous", :integer, :limit => 4, :default => 0, :null => false
+ t.column "startdate", :datetime, :null => false
+ t.column "enddate", :datetime
+ end
+
+ create_table "rankings", :force => true do |t|
+ t.column "vote_id", :integer
+ t.column "candidate_id", :integer
+ t.column "rank", :integer
+ end
+
+ create_table "tokens", :force => true do |t|
+ t.column "token", :string, :limit => 100, :default => "", :null => false
+ t.column "vote_id", :integer, :default => 0, :null => false
+ end
+
+ add_index "tokens", ["vote_id"], :name => "fk_vote_token"
+
+ create_table "users", :force => true do |t|
+ t.column "login", :string, :limit => 80, :default => "", :null => false
+ t.column "salted_password", :string, :limit => 40, :default => "", :null => false
+ t.column "email", :string, :limit => 60, :default => "", :null => false
+ t.column "firstname", :string, :limit => 40
+ t.column "lastname", :string, :limit => 40
+ t.column "salt", :string, :limit => 40, :default => "", :null => false
+ t.column "verified", :integer, :default => 0
+ t.column "role", :string, :limit => 40
+ t.column "security_token", :string, :limit => 40
+ t.column "token_expiry", :datetime
+ t.column "created_at", :datetime
+ t.column "updated_at", :datetime
+ t.column "logged_in_at", :datetime
+ t.column "deleted", :integer, :default => 0
+ t.column "delete_after", :datetime
+ end
+
+ create_table "voters", :force => true do |t|
+ t.column "email", :string, :limit => 100, :default => "", :null => false
+ t.column "password", :string, :limit => 100, :default => "", :null => false
+ t.column "contacted", :integer, :limit => 4, :default => 0, :null => false
+ t.column "election_id", :integer, :default => 0, :null => false
+ end
+
+ add_index "voters", ["election_id"], :name => "fk_election_voter"
+
+ create_table "votes", :force => true do |t|
+ t.column "voter_id", :integer
+ t.column "confirmed", :integer, :limit => 4, :default => 0, :null => false
+ end
+
+ add_index "votes", ["voter_id"], :name => "fk_vote_voter"
+
+end
--- /dev/null
+*SVN*
+
+
+-----
+1.1.4
+
+Fixed creation of multipart emails (Ticket #190)
+Added a temporary fix to the code-mixing issue. In your engine's test/test_helper.rb, please add the following lines:
+
+ # Ensure that the code mixing and view loading from the application is disabled
+ Engines.disable_app_views_loading = true
+ Engines.disable_app_code_mixing = true
+
+is will prevent code mixing for controllers and helpers, and loading views from the application. One thing to remember is to load any controllers/helpers using 'require_or_load' in your tests, to ensure that the engine behaviour is respected (Ticket #135)
+Added tasks to easily test engines individually (Ticket #120)
+Fixture extensions will now fail with an exception if the corresponding class cannot be loaded (Ticket #138)
+Patch for new routing/controller loading in Rails 1.1.6. The routing code is now replaced with the contents of config.controller_paths, along with controller paths from any started engines (Ticket #196)
+Rails' Configuration instance is now stored, and available from all engines and plugins.
+
+
+-----
+1.1.3
+
+Fixed README to show 'models' rather than 'model' class (Ticket #167)
+Fixed dependency loading to work with Rails 1.1.4 (Ticket #180)
+
+
+-----
+1.1.2
+
+Added better fix to version checking (Ticket #130, jdell@gbdev.com).
+Fixed generated init_engine.rb so that VERSION module doesn't cause probems (Ticket #131, japgolly@gmail.com)
+Fixed error with Rails 1.0 when trying to ignore the engine_schema_info table (Ticket #132, snowblink@gmail.com)
+Re-added old style rake tasks (Ticket #133)
+No longer adding all subdirectories of <engine>/app or <engine>/lib, as this can cause issues when files are grouped in modules (Ticket #149, kasatani@gmail.com)
+Fixed engine precidence ordering for Rails 1.1 (Ticket #146)
+Added new Engines.each method to assist in processing the engines in the desired order (Ticket #146)
+Fixed annoying error message at appears when starting the console in development mode (Ticket #134)
+Engines is now super-careful about loading the correct version of Rails from vendor (Ticket #154)
+
+
+-----
+1.1.1
+
+Fixed migration rake task failing when given a specific version (Ticket #115)
+Added new rake task "test:engines" which will test engines (and other plugins) but ensure that the test database is cloned from development beforehand (Ticket #125)
+Fixed issue where 'engine_schema_info' table was included in schema dumps (Ticket #87)
+Fixed multi-part emails (Ticket #121)
+Added an 'install.rb' file to new engines created by the bundled generator, which installs the engines plugin automatically if it doesn't already exist (Ticket #122)
+Added a default VERSION module to generated engines (Ticket #123)
+Refactored copying of engine's public files to a method of an Engine instance. You can now call Engines.get(:engine_name).copy_public_files (Ticket #108)
+Changed engine generator templates from .rb files to .erb files (Ticket #106)
+Fixed the test_helper.erb file to use the correct testing extensions and not load any schema - the schema will be cloned automatically via rake test:engines
+Fixed problem when running with Rails 1.1.1 where version wasn't determined correctly (Ticket #129)
+Fixed bug preventing engines from loading when both Rails 1.1.0 and 1.1.1 gems are installed and in use.
+Updated version (d'oh!)
+
+
+-----
+1.1.0
+
+Improved regexp matching for Rails 1.0 engines with peculiar paths
+Engine instance objects can be accessed via Engines[:name], an alias for Engines.get(:name) (Ticket #99)
+init_engine.rb is now processed as the final step in the Engine.start process, so it can access files within the lib directory, which is now in the $LOAD_PATH at that point. (Ticket #99)
+Clarified MIT license (Ticket #98)
+Updated Rake tasks to integrate smoothly with Rails 1.1 namespaces
+Changed the version to "1.1.0 (svn)"
+Added more information about using the plugin with Edge Rails to the README
+moved extensions into lib/engines/ directory to enable use of Engines module in extension code.
+Added conditional require_or_load method which attempts to detect the current Rails version. To use the Edge Rails version of the loading mechanism, add the line:
+ Engines.config :edge, true
+to your environment.rb file.
+Merged changes from /branches/edge and /branches/rb_1.0 into /trunk
+engine_schema_info now respects the prefix/suffixes set for ActiveRecord::Base (Ticket #67)
+added ActiveRecord::Base.wrapped_table_name(name) method to assist in determining the correct table name
+
+
+-----
+1.0.6
+
+Added ability to determine version information for engines: rake engine_info
+Added a custom logger for the Engines module, to stop pollution of the Rails logs.
+Added some more tests (in particular, see rails_engines/applications/engines_test).
+Another attempt at solving Ticket #53 - controllers and helpers should now be loadable from modules, and if a full path (including RAILS_ROOT/ENGINES_ROOT) is given, it should be safely stripped from the require filename such that corresponding files can be located in any active engines. In other words, controller/helper overloading should now completely work, even if the controllers/helpers are in modules.
+Added (finally) patch from Ticket #22 - ActionMailer helpers should now load
+Removed support for Engines.start :engine, :engine_name => 'whatever'. It was pointless.
+Fixed engine name referencing; engine_stylesheet/engine_javascript can now happily use shorthand engine names (i.e. :test == :test_engine) (Ticket #45)
+Fixed minor documentation error ('Engine.start' ==> 'Engines.start') (Ticket #57)
+Fixed double inclusion of RAILS_ROOT in engine_migrate rake task (Ticket #61)
+Added ability to force config values even if given as a hash (Ticket #62)
+
+
+-----
+1.0.5
+
+Fixed bug stopping fixtures from loading with PostgreSQL
+
+
+-----
+1.0.4
+
+Another attempt at loading controllers within modules (Ticket #56)
+
+
+-----
+1.0.3
+
+Fixed serious dependency bug stopping controllers being loaded (Ticket #56)
+
+
+-----
+1.0.2
+
+Fixed bug with overloading controllers in modules from /app directory
+Fixed exception thrown when public files couldn't be created; exception is now logged (Ticket #52)
+Fixed problem with generated test_helper.rb file via File.expand_path (Ticket #50)
+
+
+-----
+1.0.1
+
+Added engine generator for creation of new engines
+Fixed 'Engine' typo in README
+Fixed bug in fixtures extensions
+Fixed /lib path management bug
+Added method to determine public directory location from Engine object
+Fixed bug in the error message in get_engine_dir()
+Added proper component loading
+Added preliminary tests for the config() methods module
+
+
+-----
+pre-v170
+
+Fixed copyright notices to point to DHH, rather than me.
+Moved extension require statements into lib/engines.rb, so the will be loaded if another module/file calls require 'engines
+Added a CHANGELOG file (this file)
--- /dev/null
+Copyright (c) 2006 James Adam
+
+The MIT License
+
+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.
\ No newline at end of file
--- /dev/null
+= Welcome
+
+This document gives an overview of how the Engines mechanism works within a Rails environment. In most cases the code below is just an example. For more information or documentation, please go to http://rails-engines.org.
+
+== Background
+
+Rails Engines are a way of dropping in whole chunks of functionality into your
+existing application without affecting *any* of your existing code. The could also be described as mini-applications, or vertical application slices - top-to-bottom units which provide full MVC coverage for a certain, specific application function.
+
+As an example, the Login Engine provides a full user login subsystem, including:
+* controllers to manage user accounts;
+* helpers for you to interact with account information from other
+ parts of your application;
+* the model objects and schemas to create the required tables;
+* stylesheets and javascript files to enhance the views;
+* and any other library files required.
+
+Once the Rails Core team decides on a suitable method for packaging plugins, Engines can be distributed using the same mechanisms. If you are developing engines yourself for use across multiple projects, linking them as svn externals allows seamless updating of bugfixes across multiple applications.
+
+
+
+
+
+= Edge Engines
+
+If you are using Edge Rails (an SVN copy of Rails, rather than an 'official' release), there are several MASSIVELY IMPORTANT issues that you need to bear in mind.
+
+Firstly, you are using an unstable version of Rails, so it is possible that things will break. We work hard to keep the Engines plugin up to speed with the changes in Rails at the bleeding edge, but sometimes significant parts of Rails change it WILL cause problems. This is the price of using the bleeding edge. Since edge is synonymous for unstable, the version of the Engines plugin which is compatible with Edge Rails is kept separate from the official release. Please ensure that you have used SVN to get your engines plugin from
+
+ http://svn.rails-engines.org/engines/trunk
+
+The normal 'script/plugin install engines' will NOT get you this version.
+
+Secondly, you NEED to tell the engines plugin if you expect it to perform with Edge behaviour. This is done by adding the following lines at the *very top* of environment.rb (yes, the VERY TOP)
+
+ module Engines
+ CONFIG = {:edge => true}
+ end
+
+This will set the plugin to work with Edge Rails, rather than expecting an official release.
+
+If you are having problems, please try and contribute a bug report if you can so we can improve the plugin and keep up to speed with Rails' bleeding edge. Your input is *absolutely crucial* to this. If you're not comfortable with tracking down bugs in Rails' and Engines' internal code, there is a test application available at
+
+ http://svn.rails-engines.org/applications/engines_test
+
+which contains an array of tests that might help you (and us) pinpoint where the issue is. Please download this application and consult the README for more information.
+
+Finally, please don't forget about our website and mailing lists. More information here:
+
+ http://rails-engines.org
+
+
+
+
+
+
+
+= Using the Engines plugin itself
+
+There are a number of features of the Engines plugin itself which may be useful to know:
+
+
+=== Engines.log
+The Engines plugin comes with its own logger, which is invaluable when debugging. To use it,
+simply call
+
+ Engines.create_logger
+
+Two optional arguments may be passed to this method:
+
+ Engines.create_logger(<io>)
+
+Would set the outputter to the logger to the given IO object <io>. For example, this could be STDERR or STDOUT (the default). The second argument is the logger level:
+
+ Engines.create_logger(STDOUT, Logger::INFO)
+
+The logger can be accessed using either of the following:
+
+ Engines.log.[debug|info|whatever] "message"
+ Engines.logger.[debug|info|whatever] "message"
+
+... essentially it's a Logger object. It's worth noting that if you *don't* create a logger, calls to Engines.log will just be swallowed without a sound, making it very very easy to completely silence Engine logging.
+
+
+=== Engines.config(:root)
+
+By default, the Engines plugin expects to be starting Engines from within RAILS_ROOT/vendor/plugins. However, if you'd like to store your engines in a different directory, add the following line *before* any call to Engines.start
+
+ Engines.config(:root, "/path/to/your/directory", :force)
+
+
+=== Rake Tasks
+
+The engines plugin comes with a number of handy rake tasks:
+
+ #Â display version information about the engines subsystem
+ rake engines:info
+
+ # migrate engines' database schemas in a controlled way
+ rake db:migrate:engines
+
+ # generates full documentation for all engines
+ rake doc:engines
+
+There are more, but you'll have to discover them yourself...
+
+
+== More information
+
+For more information about what you can do with the Engines plugin, you'll need to generate the documentation (rake plugindoc), or go to http://rails-engines.org. Good luck!
+
+
+
+
+
+
+
+
+= Quickstart
+
+=== Gentlemen, Start your Engines!
+
+
+Here's an *example* of how you might go about using Rails Engines. Please bear in mind that actual Engines may differ from this, but these are the steps you will *typically* have to take. Refer to individual Engine documentation for specific installation instructions. Anyway, on with the show:
+
+1. Install the Rails Engines plugin into your plugins directory. You'll probably need to accept the SSL certificate here for the OpenSVN servers. For example:
+
+ $ script/plugin install engines
+
+ or
+
+ $ svn co http://svn.rails-engines.org/plugins/engines <MY_RAILS_APP>/vendor/plugins/engines
+
+2. Install your engine into the plugins directory in a similar way.
+
+3. Create the RDoc for the engines plugin and for your engines so you know what's going on:
+
+ $ rake doc:plugins
+ $ rake doc:engines
+
+4. Initialize any database schema provided. The Engine may provide Rake tasks to do this for you. Beware that accepting an Engine schema might affect any existing database tables you have installed! You are STRONGLY recommended to inspect the <tt>db/schema.rb</tt> file to see exactly what running it might change.
+
+5. Add configuration to <tt>environment.rb</tt>:
+ e.g.
+
+ # Add your application configuration here
+ module MyEngine
+ config :top_speed, "MegaTurboFast"
+ end
+
+ Engines.start :my_engine
+
+6. Run your server!
+
+ $ script/server
+
+
+
+
+= Building an Engine
+
+Here's a sample rails application with a detailed listing of an example engines as a concrete example:
+
+ RAILS_ROOT
+ |- app
+ |- lib
+ |- config
+ |- <... other directories ...>
+ |- vendor
+ |-plugins
+ |- engines <-- the engines plugin
+ |- some_other_plugin
+ |- my_engine <-- our example engine
+ |- init_engine.rb
+ |- app
+ | |- controllers
+ | |- models
+ | |- helpers
+ | |- views
+ |- db
+ |- tasks
+ |- lib
+ |- public
+ | |- javascripts
+ | |- stylesheets
+ |- test
+
+
+The internal structure of an engine mirrors the familiar core of a Rails application, with most of the engine within the <tt>app</tt> subdirectory. Within <tt>app</tt>, the controllers, views and model objects behave just as you might expect if there in the top-level <tt>app</tt> directory.
+
+When you call <tt>Engines.start :my_engine</tt> in <tt>environment.rb</tt> a few important bits of black magic voodoo happen:
+* the engine's controllers, views and models are mixed in to your running Rails application;
+* files in the <tt>lib</tt> directory of your engine (and subdirectories) are made available
+ to the rest of your system
+* any directory structure in the folder <tt>public/</tt> within your engine is made available to the webserver
+* the file <tt>init_engine.rb</tt> is loaded from within the engine - just like a plugin. The reason why engines need an init_engine.rb rather than an init.rb is because Rails' default plugin system might try and load an engine before the Engines plugin has been loaded, resulting in all manner of badness. Instead, Rails' skips over any engine plugins, and the Engines plugin handles initializing your Engines plugins when you 'start' each engine.
+
+From within <tt>init_engine.rb</tt> you should load any libraries from your <tt>lib</tt> directory that your engine might need to function. You can also perform any configuration required.
+
+=== Loading all Engines
+
+Calling either Engines.start (with no arguments) or Engines.start_all will load all engines available. Please note that your plugin can only be *automatically* detected as an engine by the presence of an 'init_engine.rb' file, or if the engine is in a directory named <something>_engine, or <something>_bundle. If neither of these conditions hold, then your engine will not be loaded by Engines.start() (with no arguments) or Engines.start_all().
+
+
+
+
+
+
+
+= Configuring Engines
+
+Often your engine will require a number of configuration parameters set, some of which should be alterable by the user to reflect their particular needs. For example, a Login System might need a unique Salt value set to encrypt user passwords. This value should be unique to each application.
+
+Engines provides a simple mechanism to handle this, and it's already been hinted at above. Within any module, a new method is now available: <tt>config</tt>. This method creates a special <tt>CONFIG</tt> Hash object within the Module it is called, and can be used to store your parameters. For a user to set these parameters, they should reopen the module (before the corresponding Engines.start call), as follows:
+
+ module MyModule
+ config :some_option, "really_important_value"
+ end
+ Engines.start :my_engine
+
+Because this config value has been set before the Engine is started, subsequent attempts to set this config value will be ignored and the user-specified value used instead. Of course, there are situations where you *really* want to set the config value, even if it already exists. In such cases the config call can be changed to:
+
+ config :some_option, "no_THIS_really_important_value", :force
+
+The additional parameter will force the new value to be used. For more information, see Module#config.
+
+
+
+
+= Tweaking Engines
+
+One of the best things about Engines is that if you don't like the default behaviour of any component, you can override it without needing to overhaul the whole engine. This makes adding your customisations to engines almost painless, and allows for upgrading/updating engine code without affecting the specialisations you need for your particular application.
+
+
+=== View Tweaks
+These are the simplest - just drop your customised view (or partial) into you <tt>/app/views</tt> directory in the corresponding location for the engine, and your view will be used in preference to the engine view. For example, if we have a ItemController with an action 'show', it will (normally) expect to find its view as <tt>report/show.rhtml</tt> in the <tt>views</tt> directory. The view is found in the engine at <tt>/vendor/engines/my_engine/app/views/report/show.rhtml</tt>. However, if you create the file <tt>/app/views/report/show.rhtml</tt>, that file will be used instead! The same goes for partials.
+
+
+=== Controller Tweaks
+You can override controller behaviour by replacing individual controller methods with your custom behaviour. Lets say that our ItemController's 'show' method isn't up to scratch, but the rest of it behaves just fine. To override the single method, create <tt>/app/controllers/item_controller.rb</tt>, just as if it were going to be a new controller in a regular Rails application. then, implement your show method as you would like it to happen.
+
+... and that's it. Your custom code will be mixed in to the engine controller, replacing its old method with your custom code.
+
+
+=== Model/Lib Tweaks
+Alas, tweaking model objects isn't quite so easy (yet). If you need to change the behaviour of model objects, you'll need to copy the model file from the engine into <tt>/app/models</tt> and edit the methods yourself. Library code can be overridden in a similar way.
+
+
+
+
+
+
+
+
+
+= Public Files
+
+If the Engine includes a <tt>public</tt> directory, its contents will be mirrored into <tt>RAILS_ROOT/public/engine_files/<engine_name>/</tt> so that these files can be served by your webserver to browsers and users over the internet.
+
+Engines also provides two convenience methods for loading stylesheets and javascript files in your layouts: <tt>engine_stylesheet</tt> and <tt>engine_javascript</tt>.
+
+=== Engine Stylesheets
+
+ <%= engine_stylesheet "my_engine" %>
+
+will include <tt>RAILS_ROOT/public/<engine_files>/my_engine/stylesheets/my_engine.css</tt> in your layout. If you have more than one stylesheet, you can include them in the same call:
+
+ <%= engine_stylesheet "my_engine", "stylesheet_2", "another_one" %>
+
+will give you:
+
+ <link href="/engine_files/my_engine/stylesheets/my_engine.css" media="screen" rel="Stylesheet" type="text/css" />
+ <link href="/engine_files/my_engine/stylesheets/stylesheet_2.css" media="screen" rel="Stylesheet" type="text/css" />
+ <link href="/engine_files/my_engine/stylesheets/another_one.css" media="screen" rel="Stylesheet" type="text/css" />
+
+in your rendered layout.
+
+=== Engine Javascripts
+
+The <tt>engine_javascript</tt> command works in exactly the same way as above.
+
+
+
+
+
+
+
+
+
+= Databases and Engines
+
+Engine schema information can be handled using similar mechanisms to your normal application schemas.
+
+== CAVEAT EMPTOR!
+
+I.E. Big Huge Warning In Flashing Lights.
+
+PLEASE, PLEASE, PLEASE bear in mind that if you are letting someone
+ELSE have a say in what tables you are using, you're basically opening
+your application schema open to potential HAVOC. I cannot stress how
+serious this is. It is YOUR responsibility to ensure that you trust
+the schema and migration information, and that it's not going to drop
+your whole database. You need to inspect these things. YOU do. If you
+run these voodoo commands and your database essplodes because you
+didn't double double double check what was going on, your embarassment
+will be infinite. And your project will be skroo'd if the migration
+is irreversible.
+
+That said, if you are working in a team and you all trust each other,
+which is hopefully true, this can be quite useful.
+
+
+== Migrating Engines
+
+To migrate all engines to the latest version:
+
+ rake db:migrate:engines
+
+To migrate a single engine, for example the login engine:
+
+ rake db:migrate:engines ENGINE=login (or login_engine)
+
+To migrate a single engine to a specific revision:
+
+ rake db:migrate:engines ENGINE=login VERSION=4
+
+This:
+
+ rake db:migrate:engines VERSION=1
+
+... will not work, because we felt it was too dangerous to allow ALL
+engines to be migrated to a specific version, since such versions
+might be incompatible.
+
+
+
+
+
+
+
+= Testing Engines
+
+The Engines plugin comes with mechanisms to help test Engines within a developer's own application. The testing extensions enable developers to load fixtures into specific
+tables irrespective of the name of the fixtures file. This work is heavily based on
+patches made by Duane Johnson (canadaduane), viewable at
+http://dev.rubyonrails.org/ticket/1911
+
+Engine developers should supply fixture files in the <engine>/test/fixtures directory
+as normal. Within their tests, they should load the fixtures using the 'fixture' command
+(rather than the normal 'fixtures' command). For example:
+
+ class UserTest < Test::Unit::TestCase
+ fixture :users, :table_name => LoginEngine.config(:user_table), :class_name => "User"
+
+ ...
+
+This will ensure that the fixtures/users.yml file will get loaded into the correct
+table, and will use the correct model object class.
+
+Your engine should provide a test_helper.rb file in <engine>/test, the contents of which should include the following:
+
+ # Load the default rails test helper - this will load the environment.
+ require File.dirname(__FILE__) + '/../../../../test/test_helper'
+
+ # ensure that the Engines testing enhancements are loaded and will override Rails own
+ # code where needed. This line is very important!
+ require File.join(Engines.config(:root), "engines", "lib", "testing_extensions")
+
+ # Load the schema - if migrations have been performed, this will be up to date.
+ load(File.dirname(__FILE__) + "/../db/schema.rb")
+
+ # set up the fixtures location to use your engine's fixtures
+ Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
+ $LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
+
+== Loading Fixtures
+
+An additional helpful task for loading fixture data is also provided (thanks to Joe Van Dyk):
+
+ rake db:fixtures:engines:load
+ rake db:fixtures:engines:load PLUGIN=login_engine
+
+will load the engine fixture data into your development database.
+
+=== Important Caveat
+Unlike the new 'fixture' directive described above, this task currently relies on you ensuring that the table name to load fixtures into is the same as the name of the fixtures file you are trying to load. If you are using defaults, this should be fine. If you have changed table names, you will need to rename your fixtures files (and possibly update your tests to reflect this too).
+
+You should also note that fixtures typically tend to depend on test configuration information (such as test salt values), so not all data will be usable in fixture form.
+
+
+
+= LICENCE
+
+Copyright (c) 2006, James Adam
+
+The MIT License
+
+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.
\ No newline at end of file
--- /dev/null
+Description:
+ The Engine Generator creates the directories and files you need
+ to create your own engine.
+
+Example:
+ ./script/generate engine MyEngine
+
+ This will generate:
+ RAILS_ROOT
+ |- vendor
+ |-plugins
+ |- my_engine <-- our example engine
+ |- init_engine.rb
+ |- app
+ | |- controllers
+ | |- model
+ | |- helpers
+ | |- views
+ |- db
+ |- tasks
+ |- lib
+ |- public
+ | |- javascripts
+ | |- stylesheets
+ |- test
+
--- /dev/null
+# Copyright (c) 2005 Jonathan Lim <snowblink@gmail.com>
+#
+# The MIT License
+#
+# 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.
+
+
+module Rails
+ module Generator
+ module Commands
+
+ class Create < Base
+ def complex_template(relative_source, relative_destination, template_options = {})
+ options = template_options.dup
+ options[:assigns] ||= {}
+ options[:assigns]['template_for_inclusion'] = render_template_part(template_options) if template_options[:mark_id]
+ options[:assigns]['license'] = render_license(template_options)
+ template(relative_source, relative_destination, options)
+ end
+
+ def render_license(template_options)
+ # Getting Sandbox to evaluate part template in it
+ part_binding = template_options[:sandbox].call.sandbox_binding
+ part_rel_path = template_options[:insert]
+ part_path = source_path(part_rel_path)
+
+ # Render inner template within Sandbox binding
+ template_file = File.readlines(part_path)
+ case template_options[:comment_style]
+ when :rb
+ template_file.map! {|x| x.sub(/^/, '# ')}
+ end
+ rendered_part = ERB.new(template_file.join, nil, '-').result(part_binding)
+ end
+
+ end
+ end
+ end
+end
+
+
+class LicensingSandbox
+ include ActionView::Helpers::ActiveRecordHelper
+ attr_accessor :author
+
+ def sandbox_binding
+ binding
+ end
+
+end
+
+class Author
+ def initialize
+ set_name
+ set_email
+ end
+
+ def set_name
+ print "Please enter the author's name: "
+ @name = gets.chomp
+ end
+
+ def set_email
+ print "Please enter the author's email: "
+ @email = gets.chomp
+ end
+
+ def to_s
+ "#{@name} <#{@email}>"
+ end
+end
+
+class License
+ def initialize(source_root)
+ @source_root = source_root
+ select_license
+ end
+
+ def select_license
+ # list all the licenses in the licenses directory
+ licenses = Dir.entries(File.join(@source_root, 'licenses')).select { |name| name !~ /^\./ }
+ puts "We can generate the following licenses automatically for you:"
+ licenses.sort.each_with_index do |license, index|
+ puts "#{index}) #{licenses[index]}"
+ end
+ print "Please select a license: "
+ while choice = gets.chomp
+ if (choice !~ /^[0-9]+$/)
+ print "Hint - you want to be typing a number.\nPlease select a license: "
+ next
+ end
+ break if choice.to_i >=0 && choice.to_i <= licenses.length
+ end
+
+ @license = licenses[choice.to_i]
+ puts "'#{@license}' selected"
+ end
+
+ def to_s
+ File.join('licenses', @license)
+ end
+
+end
+
+class EngineGenerator < Rails::Generator::NamedBase
+
+ attr_reader :engine_class_name, :engine_underscored_name, :engine_start_name, :author
+
+
+ def initialize(runtime_args, runtime_options = {})
+ super
+ @engine_class_name = runtime_args.shift
+
+ # ensure that they've given us a valid class name
+ if @engine_class_name =~ /^[a-z]/
+ raise "'#{@engine_class_name}' should be a valid Ruby constant, e.g. 'MyEngine'; aborting generation..."
+ end
+
+ @engine_underscored_name = @engine_class_name.underscore
+ @engine_start_name = @engine_underscored_name.sub(/_engine$/, '')
+ @author = Author.new
+ @license = License.new(source_root)
+ end
+
+ def manifest
+ record do |m|
+ m.directory File.join('vendor', 'plugins')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name)
+ m.complex_template 'README',
+ File.join('vendor', 'plugins', @engine_underscored_name, 'README'),
+ :sandbox => lambda {create_sandbox},
+ :insert => @license.to_s
+
+ m.file 'install.erb', File.join('vendor', 'plugins', @engine_underscored_name, 'install.rb')
+
+ m.complex_template 'init_engine.erb',
+ File.join('vendor', 'plugins', @engine_underscored_name, 'init_engine.rb'),
+ :sandbox => lambda {create_sandbox},
+ :insert => @license.to_s,
+ :comment_style => :rb
+
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'app')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'app', 'models')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'app', 'controllers')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'app', 'helpers')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'app', 'views')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'db')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'db', 'migrate')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'lib')
+ m.complex_template File.join('lib', 'engine.erb'),
+ File.join('vendor', 'plugins', @engine_underscored_name, 'lib', "#{@engine_underscored_name}.rb"),
+ :sandbox => lambda {create_sandbox},
+ :insert => @license.to_s,
+ :comment_style => :rb
+
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'lib', @engine_underscored_name)
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'public')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'public', 'javascripts')
+ m.template File.join('public', 'javascripts', 'engine.js'), File.join('vendor', 'plugins', @engine_underscored_name, 'public', 'javascripts', "#{@engine_underscored_name}.js")
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'public', 'stylesheets')
+ m.template File.join('public', 'stylesheets', 'engine.css'), File.join('vendor', 'plugins', @engine_underscored_name, 'public', 'stylesheets', "#{@engine_underscored_name}.css")
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'tasks')
+ m.template File.join('tasks', 'engine.rake'), File.join('vendor', 'plugins', @engine_underscored_name, 'tasks', "#{@engine_underscored_name}.rake")
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'test')
+ m.template File.join('test', 'test_helper.erb'), File.join('vendor', 'plugins', @engine_underscored_name, 'test', 'test_helper.rb')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'test', 'fixtures')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'test', 'functional')
+ m.directory File.join('vendor', 'plugins', @engine_underscored_name, 'test', 'unit')
+ end
+ end
+
+protected
+ def banner
+ "Usage: #{$0} #{spec.name} MyEngine [general options]"
+ end
+
+ def create_sandbox
+ sandbox = LicensingSandbox.new
+ sandbox.author = @author
+ sandbox
+ end
+
+end
--- /dev/null
+------------------[ once you've read this, delete it ]---------------------
+
+ENGINE DEVELOPERS - HEED THIS!
+
+This is a sample README file to guide your users when they are installing what is undoubtedly going to be the finest piece of code they ever got their hands on. Lucky them, but alas they are often foolish, and so this is where you can guide them with the metaphorical beating of twigs. Or just a numbered series of instructions.
+
+ANYWAY - you will almost certainly need to tailor this to your specific engine. For instance, your users will probably only need to include modules into the ApplicationController and ApplicationHelper if your engine defines methods to be usable by controllers and views external to your engine.
+
+If you engine does not rely on any database tables, you will probably not need migrations either.
+
+You are also under no obligation to use the 'config' method for setting options within your modules. Documentation which explains the purpose of the 'config' method is available as part of the Engines plugin itself.
+
+Please check the engine development information on the Rails Engines wiki for more information about what to do now:
+
+ http://rails-engines.rubyforge.org/wiki/wiki.pl?DevelopingAnEngine
+
+-----------------------[ remember to delete me! ]--------------------------
+
+= <%= engine_class_name %>
+
+<%= engine_class_name %> is a ...
+
+This software package is developed using the Engines plugin. To find out more about how to use engines in general, go to http://rails-engines.rubyforge.org for general documentation about the Engines mechanism.
+
+== Installation
+
+1. Create your Rails application, set up your databases, grab the Engines plugin and the <%= engine_class_name %>, and install them.
+
+2. Install the <%= engine_class_name %> into your vendor/plugins directory
+
+3. Modify your Engines.start call in config/environment.rb
+
+ Engines.start :<%= engine_start_name %> # or :<%= engine_underscored_name %>
+
+4. Edit your application.rb file so it looks something like the following:
+
+ class ApplicationController < ActionController::Base
+ include <%= engine_class_name %>
+ end
+
+5. Edit your application_helper.rb file:
+
+ module ApplicationHelper
+ include <%= engine_class_name %>
+ end
+
+6. Perform any configuration you might need. You'll probably want to set these values in environment.rb (before the call to Engines.start):
+
+ module <%= engine_class_name %>
+ config :some_option, "some_value"
+ end
+
+7. Initialize the database tables. You can either use the engine migrations by calling:
+
+ rake engine_migrate
+
+ to move all engines to their latest versions, or
+
+ rake engine_migrate ENGINE=<%= engine_start_name %>
+
+ to migrate only this engine.
+
+8. The <%= engine_class_name %> provides a default stylesheet and a small javascript helper file, so you'll probably want to include the former and almost certainly the latter in your application's layout. Add the following lines:
+
+ <%%= engine_stylesheet "<%=engine_start_name%>_engine" %>
+ <%%= engine_javascript "<%=engine_start_name%>_engine" %>
+
+== Configuration
+
+A number of configuration parameters are available to allow to you control
+how the data is stored, should you be unhappy with the defaults. These are
+outlined below.
+
+ module <%= engine_class_name %>
+ config :some_option, "some_value"
+ end
+
+=== Configuration Options
++some_option+:: This option will set some_value
+
+== Usage
+How to use this engine
+
+== License
+<%= license %>
--- /dev/null
+<%= license %>
+
+module <%= engine_class_name %>
+ module VERSION
+ Major = 0 # change implies compatibility breaking with previous versions
+ Minor = 1 # change implies backwards-compatible change to API
+ Release = 0 # incremented with bug-fixes, updates, etc.
+ end
+end
+
+Engines.current.version = <%= engine_class_name %>::VERSION
+
+# load up all the required files we need...
+require '<%= engine_underscored_name %>'
+
--- /dev/null
+# Install the engines plugin if it has been already
+unless File.exist?(File.dirname(__FILE__) + "/../engines")
+ Commands::Plugin.parse!(['install', 'http://svn.rails-engines.org/plugins/engines'])
+end
\ No newline at end of file
--- /dev/null
+<%= license %>
+
+#require '<%= engine_underscored_name %>/additional_files'
+
+module <%= engine_class_name %>
+end
--- /dev/null
+Copyright (c) <%= Time.now.strftime("%Y") %> <%= author %>
+
+The GNU General Public License (GPL)
+Version 2, June 1991
+
+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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
--- /dev/null
+Copyright (c) <%= Time.now.strftime("%Y") %> <%= author %>
+
+GNU Lesser General Public License
+Version 2.1, February 1999
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
--- /dev/null
+Copyright (c) <%= Time.now.strftime("%Y") %> <%= author %>
+
+The MIT License
+
+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.
+
--- /dev/null
+Copyright (c) <%= Time.now.strftime("%Y") %> <%= author %>
--- /dev/null
+require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper') # the default rails helper
+
+# ensure that the Engines testing enhancements are loaded.
+require File.join(Engines.config(:root), "engines", "lib", "engines", "testing_extensions")
+
+# Ensure that the code mixing and view loading from the application is disabled
+Engines.disable_app_views_loading = true
+Engines.disable_app_code_mixing = true
+
+# force these config values
+module <%= engine_class_name %>
+# config :some_option, "some_value"
+end
+
+# set up the fixtures location
+Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
+$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
\ No newline at end of file
--- /dev/null
+#--
+# Copyright (c) 2006 James Adam
+#
+# 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.
+#
+#
+#
+# = IN OTHER WORDS:
+#
+# You are free to use this software as you please, but if it breaks you'd
+# best not come a'cryin...
+#++
+
+# Load the engines & bundles extensions
+require 'engines'
+require 'bundles'
+
+module ::Engines::Version
+ Major = 1 # change implies compatibility breaking with previous versions
+ Minor = 1 # change implies backwards-compatible change to API
+ Release = 4 # incremented with bug-fixes, updates, etc.
+end
+
+#--
+# Create the Engines directory if it isn't present
+#++
+if !File.exist?(Engines.config(:root))
+ Engines.log.debug "Creating engines directory in /vendor"
+ FileUtils.mkdir_p(Engines.config(:root))
+end
+
+# Keep a hold of the Rails Configuration object
+Engines.rails_config = config
+
+# Initialize the routing (controller_paths)
+Engines.initialize_routing
\ No newline at end of file
--- /dev/null
+require 'bundles/require_resource'
+
+# The 'require_bundle' method is used in views to declare that certain stylesheets and javascripts should
+# be included by the 'resource_tags' (used in the layout) for the view to function properly.
+module Bundles
+ def require_bundle(name, *args)
+ method = "bundle_#{name}"
+ send(method, *args)
+ end
+
+ def require_bundles(*names)
+ names.each { |name| require_bundle(name) }
+ end
+end
+
+ActionView::Base.send(:include, Bundles)
+
+# Registers a module within the Bundles module by renaming the module's 'bundle' method (so it doesn't
+# clash with other methods named 'bundle') and by including any Controller or Helper modules within
+# their respective Rails base classes.
+#
+# For example, if you have a module such as
+# module Bundles::Calendar; end
+#
+# then within that Calendar module there *must* be a method named "bundle" which groups the
+# bundle's resources together. Example:
+# module Bundles::Calendar
+# def bundle
+# require_relative_to Engines.current.public_dir do
+# require_stylesheet "/stylesheets/calendar.css"
+# require_javascript "/javascripts/calendar.js"
+# end
+# end
+# end
+#
+# You may optionally define a Controller or Helper sub-module if you need any methods available to
+# the applications controllers or views. Example:
+#
+# module Bundles::Calendar
+# module Helper
+# def calendar_date_select(*args
+# # ... output some HTML
+# end
+# end
+# end
+#
+# The calendar_date_select method will now be available within the scope of the app's views because the
+# register_bundle method will inject the Helper module's methods in to ActionView::Base for you.
+#
+# Similarly, you can make methods available to controllers by adding a Controller module.
+def register_bundle(name)
+ require "bundles/#{name}"
+
+ # Rename the generic 'bundle' method in to something that doesn't conflict with
+ # the other module method names.
+ bundle_module = Bundles.const_get(name.to_s.camelize)
+ bundle_module.module_eval "alias bundle_#{name} bundle"
+ bundle_module.send :undef_method, :bundle
+
+ # Then include the bundle module in to the base module, so that the methods will
+ # be available inside ActionView::Base
+ ActionView::Base.send(:include, bundle_module)
+
+ # Check for optional Controller module
+ if bundle_module.const_defined? 'Controller'
+ controller_addon = bundle_module.const_get('Controller')
+ RAILS_DEFAULT_LOGGER.debug "Including #{name} bundle's Controller module"
+ ActionController::Base.send(:include, controller_addon)
+ end
+
+ # Check for optional Helper module
+ if bundle_module.const_defined? 'Helper'
+ helper_addon = bundle_module.const_get('Helper')
+ RAILS_DEFAULT_LOGGER.debug "Including #{name} bundle's Helper module"
+ ActionView::Base.send(:include, helper_addon)
+ end
+end
\ No newline at end of file
--- /dev/null
+# RequireResource v.1.4 by Duane Johnson
+#
+# Makes inclusion of javascript and stylesheet resources easier via automatic or explicit
+# calls. e.g. require_javascript 'popup' is an explicit call.
+#
+# The simplest way to make use of this functionality is to add
+# <%= resource_tags %>
+# to your layout (usually in the <head></head> section). This will automatically add
+# bundle support to your application, as well as enable simple javascript and stylesheet
+# dependencies for your views.
+#
+# Note that this can easily be turned in to a helper on its own.
+module RequireResource
+ mattr_accessor :path_prefix
+
+ # Write out all javascripts & stylesheets, including default javascripts (unless :defaults => false)
+ def resource_tags(options = {})
+ options = {:auto => true, :defaults => true}.update(options)
+ require_defaults if options[:defaults]
+ stylesheet_auto_link_tags(:auto => options[:auto]) +
+ javascript_auto_include_tags(:auto => options[:auto])
+ end
+
+ # Write out the <link> tags themselves based on the array of stylesheets to be included
+ def stylesheet_auto_link_tags(options = {})
+ options = {:auto => true}.update(options)
+ ensure_resource_is_initialized(:stylesheet)
+ autorequire(:stylesheet) if options[:auto]
+ @stylesheets.uniq.inject("") do |buffer, css|
+ buffer << stylesheet_link_tag(css) + "\n "
+ end
+ end
+
+ # Write out the <script> tags themselves based on the array of javascripts to be included
+ def javascript_auto_include_tags(options = {})
+ options = {:auto => true}.update(options)
+ ensure_resource_is_initialized(:javascript)
+ autorequire(:javascript) if options[:auto]
+ @javascripts.uniq.inject("") do |buffer, js|
+ buffer << javascript_include_tag(js) + "\n "
+ end
+ end
+
+ # Bundle the defaults together for easy inclusion
+ def require_defaults
+ require_javascript(:prototype)
+ require_javascript(:controls)
+ require_javascript(:effects)
+ require_javascript(:dragdrop)
+ end
+
+ # Adds a javascript to the array of javascripts that will be included in the layout by
+ # either your call to 'javascript_auto_include_tags' or 'resource_tags'.
+ def require_javascript(*scripts)
+ scripts.each do |script|
+ require_resource(:javascript, RequireResource.path_prefix.to_s + script.to_s)
+ end
+ end
+
+ # Adds a stylesheet to the array of stylesheets that will be included in the layout by
+ # either your call to 'stylesheet_auto_link_tags' or 'resource_tags'.
+ def require_stylesheet(*sheets)
+ sheets.each do |sheet|
+ require_resource(:stylesheet, RequireResource.path_prefix.to_s + sheet.to_s)
+ end
+ end
+
+ # Changes the RequireResource.path_prefix within the scope of a block. This is
+ # particularly useful when requiring several resources within a directory. For example,
+ # bundles can take advantage of this by calling
+ # require_relative_to Engines.current.public_dir do
+ # require_javascript '...'
+ # require_stylesheet '...'
+ # # ...
+ # end
+ def require_relative_to(path)
+ former_prefix = RequireResource.path_prefix
+ RequireResource.path_prefix = path
+ yield
+ RequireResource.path_prefix = former_prefix
+ end
+
+ protected
+ # Adds resources such as stylesheet or javascript files to the corresponding array of
+ # resources that will be 'required' by the layout. The +resource_type+ is either
+ # :javascript or :stylesheet. The +extension+ is optional, and should normally correspond
+ # with the resource type, e.g. 'css' for :stylesheet and 'js' for :javascript.
+ def autorequire(resource_type, extension = nil)
+ extensions = {:stylesheet => 'css', :javascript => 'js'}
+ extension ||= extensions[resource_type]
+ candidates = []
+ class_iterator = controller.class
+ resource_path = "#{RAILS_ROOT}/public/#{resource_type.to_s.pluralize}/"
+
+ while ![ActionController::Base].include? class_iterator
+ controller_path = class_iterator.to_s.underscore.sub('controllers/', '').sub('_controller', '')
+ candidates |= [ "#{controller_path}", "#{controller_path}/#{controller.action_name}" ]
+ class_iterator = class_iterator.superclass
+ end
+
+ for candidate in candidates
+ if FileTest.exist?("#{resource_path}/#{candidate}.#{extension}")
+ require_resource(resource_type, candidate)
+ end
+ end
+ end
+
+ # Adds a resource (e.g. a javascript) to the appropriate array (e.g. @javascripts)
+ # ONLY if the resource is not already included.
+ def require_resource(type, name)
+ variable = type.to_s.pluralize
+ new_resource_array = (instance_variable_get("@#{variable}") || []) | [name.to_s]
+ instance_variable_set("@#{variable}", new_resource_array)
+ end
+
+ # Ensures that a resource array (e.g. @javascripts) is not nil--uses [] if so
+ def ensure_resource_is_initialized(type)
+ variable = type.to_s.pluralize
+ new_resource_array = (instance_variable_get("@#{variable}") || [])
+ instance_variable_set("@#{variable}", new_resource_array)
+ end
+end
+
+ActionView::Base.send(:include, RequireResource)
--- /dev/null
+require 'logger'
+
+# We need to know the version of Rails that we are running before we
+# can override any of the dependency stuff, since Rails' own behaviour
+# has changed over the various releases. We need to explicily make sure
+# that the Rails::VERSION constant is loaded, because such things could
+# not automatically be achieved prior to 1.1, and the location of the
+# file moved in 1.1.1!
+def load_rails_version
+ # At this point, we can't even rely on RAILS_ROOT existing, so we have to figure
+ # the path to RAILS_ROOT/vendor/rails manually
+ rails_base = File.expand_path(
+ File.join(File.dirname(__FILE__), # RAILS_ROOT/vendor/plugins/engines/lib
+ '..', # RAILS_ROOT/vendor/plugins/engines
+ '..', # RAILS_ROOT/vendor/plugins
+ '..', # RAILS_ROOT/vendor
+ 'rails', 'railties', 'lib')) # RAILS_ROOT/vendor/rails/railties/lib
+ begin
+ load File.join(rails_base, 'rails', 'version.rb')
+ #puts 'loaded 1.1.1+ from vendor: ' + File.join(rails_base, 'rails', 'version.rb')
+ rescue MissingSourceFile # this means they DON'T have Rails 1.1.1 or later installed in vendor
+ begin
+ load File.join(rails_base, 'rails_version.rb')
+ #puts 'loaded 1.1.0- from vendor: ' + File.join(rails_base, 'rails_version.rb')
+ rescue MissingSourceFile # this means they DON'T have Rails 1.1.0 or previous installed in vendor
+ begin
+ # try and load version information for Rails 1.1.1 or later from the $LOAD_PATH
+ require 'rails/version'
+ #puts 'required 1.1.1+ from load path'
+ rescue LoadError
+ # try and load version information for Rails 1.1.0 or previous from the $LOAD_PATH
+ require 'rails_version'
+ #puts 'required 1.1.0- from load path'
+ end
+ end
+ end
+end
+
+# Actually perform the load
+load_rails_version
+#puts "Detected Rails version: #{Rails::VERSION::STRING}"
+
+require 'engines/ruby_extensions'
+# ... further files are required at the bottom of this file
+
+# Holds the Rails Engine loading logic and default constants
+module Engines
+
+ class << self
+ # Return the version string for this plugin
+ def version
+ "#{Version::Major}.#{Version::Minor}.#{Version::Release}"
+ end
+
+ # For holding the rails configuration object
+ attr_accessor :rails_config
+
+ # A flag to stop searching for views in the application
+ attr_accessor :disable_app_views_loading
+
+ # A flag to stop code being mixed in from the application
+ attr_accessor :disable_app_code_mixing
+ end
+
+ # The DummyLogger is a class which might pass through to a real Logger
+ # if one is assigned. However, it can gracefully swallow any logging calls
+ # if there is now Logger assigned.
+ class LoggerWrapper
+ def initialize(logger=nil)
+ set_logger(logger)
+ end
+ # Assign the 'real' Logger instance that this dummy instance wraps around.
+ def set_logger(logger)
+ @logger = logger
+ end
+ # log using the appropriate method if we have a logger
+ # if we dont' have a logger, ignore completely.
+ def method_missing(name, *args)
+ if @logger && @logger.respond_to?(name)
+ @logger.send(name, *args)
+ end
+ end
+ end
+
+ LOGGER = Engines::LoggerWrapper.new
+
+ class << self
+ # Create a new Logger instance for Engines, with the given outputter and level
+ def create_logger(outputter=STDOUT, level=Logger::INFO)
+ LOGGER.set_logger(Logger.new(outputter, level))
+ end
+ # Sets the Logger instance that Engines will use to send logging information to
+ def set_logger(logger)
+ Engines::LOGGER.set_logger(logger) # TODO: no need for Engines:: part
+ end
+ # Retrieves the current Logger instance
+ def log
+ Engines::LOGGER # TODO: no need for Engines:: part
+ end
+ alias :logger :log
+ end
+
+ # An array of active engines. This should be accessed via the Engines.active method.
+ ActiveEngines = []
+
+ # The root directory for engines
+ config :root, File.join(RAILS_ROOT, "vendor", "plugins")
+
+ # The name of the public folder under which engine files are copied
+ config :public_dir, "engine_files"
+
+ class << self
+
+ # Initializes a Rails Engine by loading the engine's init.rb file and
+ # ensuring that any engine controllers are added to the load path.
+ # This will also copy any files in a directory named 'public'
+ # into the public webserver directory. Example usage:
+ #
+ # Engines.start :login
+ # Engines.start :login_engine # equivalent
+ #
+ # A list of engine names can be specified:
+ #
+ # Engines.start :login, :user, :wiki
+ #
+ # The engines will be loaded in the order given.
+ # If no engine names are given, all engines will be started.
+ #
+ # Options can include:
+ # * :copy_files => true | false
+ #
+ # Note that if a list of engines is given, the options will apply to ALL engines.
+ def start(*args)
+
+ options = (args.last.is_a? Hash) ? args.pop : {}
+
+ if args.empty?
+ start_all
+ else
+ args.each do |engine_name|
+ start_engine(engine_name, options)
+ end
+ end
+ end
+
+ # Starts all available engines. Plugins are considered engines if they
+ # include an init_engine.rb file, or they are named <something>_engine.
+ def start_all
+ plugins = Dir[File.join(config(:root), "*")]
+ Engines.log.debug "considering plugins: #{plugins.inspect}"
+ plugins.each { |plugin|
+ engine_name = File.basename(plugin)
+ if File.exist?(File.join(plugin, "init_engine.rb")) || # if the directory contains init_engine.rb
+ (engine_name =~ /_engine$/) || # or it engines in '_engines'
+ (engine_name =~ /_bundle$/) # or even ends in '_bundle'
+
+ start(engine_name) # start the engine...
+
+ end
+ }
+ end
+
+ # Initialize the routing controller paths.
+ def initialize_routing
+ # See lib/engines/routing_extensions.rb for more information.
+ ActionController::Routing.controller_paths = Engines.rails_config.controller_paths
+ end
+
+ def start_engine(engine_name, options={})
+
+ # Create a new Engine and put this engine at the front of the ActiveEngines list
+ current_engine = Engine.new(engine_name)
+ Engines.active.unshift current_engine
+ Engines.log.info "Starting engine '#{current_engine.name}' from '#{File.expand_path(current_engine.root)}'"
+
+ # add the code directories of this engine to the load path
+ add_engine_to_load_path(current_engine)
+
+ # add the controller & component path to the Dependency system
+ engine_controllers = File.join(current_engine.root, 'app', 'controllers')
+ engine_components = File.join(current_engine.root, 'components')
+
+
+ # This mechanism is no longer required in Rails trunk
+ if Rails::VERSION::STRING =~ /^1.0/ && !Engines.config(:edge)
+ Controllers.add_path(engine_controllers) if File.exist?(engine_controllers)
+ Controllers.add_path(engine_components) if File.exist?(engine_components)
+ else
+ ActionController::Routing.controller_paths << engine_controllers
+ ActionController::Routing.controller_paths << engine_components
+ end
+
+ # copy the files unless indicated otherwise
+ if options[:copy_files] != false
+ current_engine.mirror_engine_files
+ end
+
+ # load the engine's init.rb file
+ startup_file = File.join(current_engine.root, "init_engine.rb")
+ if File.exist?(startup_file)
+ eval(IO.read(startup_file), binding, startup_file)
+ # possibly use require_dependency? Hmm.
+ else
+ Engines.log.debug "No init_engines.rb file found for engine '#{current_engine.name}'..."
+ end
+ end
+
+ # Adds all directories in the /app and /lib directories within the engine
+ # to the load path
+ def add_engine_to_load_path(engine)
+
+ # remove the lib directory added by load_plugin, and place it in the corrent
+ # location *after* the application/lib. This can be removed when
+ # http://dev.rubyonrails.org/ticket/2910 is fixed.
+ app_lib_index = $LOAD_PATH.index(File.join(RAILS_ROOT, "lib"))
+ engine_lib = File.join(engine.root, "lib")
+ if app_lib_index
+ $LOAD_PATH.delete(engine_lib)
+ $LOAD_PATH.insert(app_lib_index+1, engine_lib)
+ end
+
+ # Add ALL paths under the engine root to the load path
+ app_dirs = %w(controllers helpers models).collect { |d|
+ File.join(engine.root, 'app', d)
+ }
+ other_dirs = %w(components lib).collect { |d|
+ File.join(engine.root, d)
+ }
+ load_paths = (app_dirs + other_dirs).select { |d| File.directory?(d) }
+
+ # Remove other engines from the $LOAD_PATH by matching against the engine.root values
+ # in ActiveEngines. Store the removed engines in the order they came off.
+
+ old_plugin_paths = []
+ # assumes that all engines are at the bottom of the $LOAD_PATH
+ while (File.expand_path($LOAD_PATH.last).index(File.expand_path(Engines.config(:root))) == 0) do
+ old_plugin_paths.unshift($LOAD_PATH.pop)
+ end
+
+
+ # add these LAST on the load path.
+ load_paths.reverse.each { |dir|
+ if File.directory?(dir)
+ Engines.log.debug "adding #{File.expand_path(dir)} to the load path"
+ #$LOAD_PATH.push(File.expand_path(dir))
+ $LOAD_PATH.push dir
+ end
+ }
+
+ # Add the other engines back onto the bottom of the $LOAD_PATH. Put them back on in
+ # the same order.
+ $LOAD_PATH.push(*old_plugin_paths)
+ $LOAD_PATH.uniq!
+ end
+
+ # Returns the directory in which all engine public assets are mirrored.
+ def public_engine_dir
+ File.expand_path(File.join(RAILS_ROOT, "public", Engines.config(:public_dir)))
+ end
+
+ # create the /public/engine_files directory if it doesn't exist
+ def create_base_public_directory
+ if !File.exists?(public_engine_dir)
+ # create the public/engines directory, with a warning message in it.
+ Engines.log.debug "Creating public engine files directory '#{public_engine_dir}'"
+ FileUtils.mkdir(public_engine_dir)
+ File.open(File.join(public_engine_dir, "README"), "w") do |f|
+ f.puts <<EOS
+Files in this directory are automatically generated from your Rails Engines.
+They are copied from the 'public' directories of each engine into this directory
+each time Rails starts (server, console... any time 'start_engine' is called).
+Any edits you make will NOT persist across the next server restart; instead you
+should edit the files within the <engine_name>/public/ directory itself.
+EOS
+ end
+ end
+ end
+
+ # Returns the Engine object for the specified engine, e.g.:
+ # Engines.get(:login)
+ def get(name)
+ active.find { |e| e.name == name.to_s || e.name == "#{name}_engine" }
+ end
+ alias_method :[], :get
+
+ # Returns the Engine object for the current engine, i.e. the engine
+ # in which the currently executing code lies.
+ def current
+ current_file = caller[0]
+ active.find do |engine|
+ File.expand_path(current_file).index(File.expand_path(engine.root)) == 0
+ end
+ end
+
+ # Returns an array of active engines
+ def active
+ ActiveEngines
+ end
+
+ # Pass a block to perform an operation on each engine. You may pass an argument
+ # to determine the order:
+ #
+ # * :load_order - in the order they were loaded (i.e. lower precidence engines first).
+ # * :precidence_order - highest precidence order (i.e. last loaded) first
+ def each(ordering=:precidence_order, &block)
+ engines = (ordering == :load_order) ? active.reverse : active
+ engines.each { |e| yield e }
+ end
+ end
+end
+
+# A simple class for holding information about loaded engines
+class Engine
+
+ # Returns the base path of this engine
+ attr_accessor :root
+
+ # Returns the name of this engine
+ attr_reader :name
+
+ # An attribute for holding the current version of this engine. There are three
+ # ways of providing an engine version. The simplest is using a string:
+ #
+ # Engines.current.version = "1.0.7"
+ #
+ #Â Alternatively you can set it to a module which contains Major, Minor and Release
+ # constants:
+ #
+ # module LoginEngine::Version
+ # Major = 1; Minor = 0; Release = 6;
+ # end
+ # Engines.current.version = LoginEngine::Version
+ #
+ # Finally, you can set it to your own Proc, if you need something really fancy:
+ #
+ # Engines.current.version = Proc.new { File.open('VERSION', 'r').readlines[0] }
+ #
+ attr_writer :version
+
+ # Engine developers can store any information they like in here.
+ attr_writer :info
+
+ # Creates a new object holding information about an Engine.
+ def initialize(name)
+
+ @root = ''
+ suffixes = ['', '_engine', '_bundle']
+ while !File.exist?(@root) && !suffixes.empty?
+ suffix = suffixes.shift
+ @root = File.join(Engines.config(:root), name.to_s + suffix)
+ end
+
+ if !File.exist?(@root)
+ raise "Cannot find the engine '#{name}' in either /vendor/plugins/#{name}, " +
+ "/vendor/plugins/#{name}_engine or /vendor/plugins/#{name}_bundle."
+ end
+
+ @name = File.basename(@root)
+ end
+
+ # Returns the version string of this engine
+ def version
+ case @version
+ when Module
+ "#{@version::Major}.#{@version::Minor}.#{@version::Release}"
+ when Proc # not sure about this
+ @version.call
+ when NilClass
+ 'unknown'
+ else
+ @version
+ end
+ end
+
+ # Returns a string describing this engine
+ def info
+ @info || '(none)'
+ end
+
+ # Returns a string representation of this engine
+ def to_s
+ "Engine<'#{@name}' [#{version}]:#{root.gsub(RAILS_ROOT, '')}>"
+ end
+
+ # return the path to this Engine's public files (with a leading '/' for use in URIs)
+ def public_dir
+ File.join("/", Engines.config(:public_dir), name)
+ end
+
+ # Replicates the subdirectories under the engine's /public directory into
+ # the corresponding public directory.
+ def mirror_engine_files
+
+ begin
+ Engines.create_base_public_directory
+
+ source = File.join(root, "public")
+ Engines.log.debug "Attempting to copy public engine files from '#{source}'"
+
+ # if there is no public directory, just return after this file
+ return if !File.exist?(source)
+
+ source_files = Dir[source + "/**/*"]
+ source_dirs = source_files.select { |d| File.directory?(d) }
+ source_files -= source_dirs
+
+ Engines.log.debug "source dirs: #{source_dirs.inspect}"
+
+ # Create the engine_files/<something>_engine dir if it doesn't exist
+ new_engine_dir = File.join(RAILS_ROOT, "public", public_dir)
+ if !File.exists?(new_engine_dir)
+ # Create <something>_engine dir with a message
+ Engines.log.debug "Creating #{public_dir} public dir"
+ FileUtils.mkdir_p(new_engine_dir)
+ end
+
+ # create all the directories, transforming the old path into the new path
+ source_dirs.uniq.each { |dir|
+ begin
+ # strip out the base path and add the result to the public path, i.e. replace
+ # ../script/../vendor/plugins/engine_name/public/javascript
+ # with
+ # engine_name/javascript
+ #
+ relative_dir = dir.gsub(File.join(root, "public"), name)
+ target_dir = File.join(Engines.public_engine_dir, relative_dir)
+ unless File.exist?(target_dir)
+ Engines.log.debug "creating directory '#{target_dir}'"
+ FileUtils.mkdir_p(target_dir)
+ end
+ rescue Exception => e
+ raise "Could not create directory #{target_dir}: \n" + e
+ end
+ }
+
+ # copy all the files, transforming the old path into the new path
+ source_files.uniq.each { |file|
+ begin
+ # change the path from the ENGINE ROOT to the public directory root for this engine
+ target = file.gsub(File.join(root, "public"),
+ File.join(Engines.public_engine_dir, name))
+ unless File.exist?(target) && FileUtils.identical?(file, target)
+ Engines.log.debug "copying file '#{file}' to '#{target}'"
+ FileUtils.cp(file, target)
+ end
+ rescue Exception => e
+ raise "Could not copy #{file} to #{target}: \n" + e
+ end
+ }
+ rescue Exception => e
+ Engines.log.warn "WARNING: Couldn't create the engine public file structure for engine '#{name}'; Error follows:"
+ Engines.log.warn e
+ end
+ end
+end
+
+
+# These files must be required after the Engines module has been defined.
+require 'engines/dependencies_extensions'
+require 'engines/routing_extensions'
+require 'engines/action_view_extensions'
+require 'engines/action_mailer_extensions'
+require 'engines/migration_extensions'
+require 'engines/active_record_extensions'
+
+# only load the testing extensions if we are in the test environment
+require 'engines/testing_extensions' if %w(test).include?(RAILS_ENV)
--- /dev/null
+# Overriding ActionMailer to teach it about Engines...
+module ActionMailer
+ class Base
+
+ # Initialize the mailer via the given +method_name+. The body will be
+ # rendered and a new TMail::Mail object created.
+ def create!(method_name, *parameters) #:nodoc:
+ initialize_defaults(method_name)
+ send(method_name, *parameters)
+
+
+ # If an explicit, textual body has not been set, we check assumptions.
+ unless String === @body
+ # First, we look to see if there are any likely templates that match,
+ # which include the content-type in their file name (i.e.,
+ # "the_template_file.text.html.rhtml", etc.). Only do this if parts
+ # have not already been specified manually.
+
+ templates = get_all_templates_for_action(@template)
+
+ #RAILS_DEFAULT_LOGGER.debug "template: #{@template}; templates: #{templates.inspect}"
+
+ if @parts.empty?
+
+ # /app/views/<mailer object name> / <action>.something.rhtml
+
+ #templates = Dir.glob("#{template_path}/#{@template}.*")
+
+ # this loop expects an array of paths to actual template files which match
+ # the given action name
+ templates.each do |path|
+ # TODO: don't hardcode rhtml|rxml
+ basename = File.basename(path)
+ next unless md = /^([^\.]+)\.([^\.]+\.[^\+]+)\.(rhtml|rxml)$/.match(basename)
+
+ template_name = basename
+ content_type = md.captures[1].gsub('.', '/')
+
+ @parts << Part.new(:content_type => content_type,
+ :disposition => "inline", :charset => charset,
+ :body => render_message(template_name, @body))
+ end
+ unless @parts.empty?
+ @content_type = "multipart/alternative"
+ @parts = sort_parts(@parts, @implicit_parts_order)
+ end
+ end
+
+ # Then, if there were such templates, we check to see if we ought to
+ # also render a "normal" template (without the content type). If a
+ # normal template exists (or if there were no implicit parts) we render
+ # it.
+ template_exists = @parts.empty?
+ # template_exists ||= Dir.glob("#{template_path}/#{@template}.*").any? { |i| i.split(".").length == 2 }
+ template_exists ||= templates.any? do |i|
+ arr = File.basename(i).split(".")
+ (arr.length == 2) && (arr[0] == @template)
+ end
+ @body = render_message(@template, @body) if template_exists
+
+ # Finally, if there are other message parts and a textual body exists,
+ # we shift it onto the front of the parts and set the body to nil (so
+ # that create_mail doesn't try to render it in addition to the parts).
+ if !@parts.empty? && String === @body
+ @parts.unshift Part.new(:charset => charset, :body => @body)
+ @body = nil
+ end
+ end
+
+ # If this is a multipart e-mail add the mime_version if it is not
+ # already set.
+ @mime_version ||= "1.0" if !@parts.empty?
+
+ # build the mail object itself
+ @mail = create_mail
+ end
+
+ private
+
+
+ # JGA - Modified to pass the method name to initialize_template_class
+ def render(opts)
+ body = opts.delete(:body)
+ initialize_template_class(body, opts[:file]).render(opts)
+ end
+
+
+ # Return all ActionView template paths from the app and all Engines
+ def template_paths
+ paths = [template_path]
+ Engines.each { |engine|
+ # add a path for every engine if one exists.
+ engine_template_path = File.join(engine.root, "app", "views", mailer_name)
+ paths << engine_template_path if File.exists?(engine_template_path)
+ }
+ paths
+ end
+
+ # Returns a list of all template paths in the app and Engines
+ # which contain templates that might be used for the given action
+ def get_all_templates_for_action(action)
+ # can we trust uniq! to do this? i'm not sure...
+ templates = []
+ seen_names = []
+ template_paths.each { |path|
+ all_templates_for_path = Dir.glob(File.join(path, "#{action}*"))
+ all_templates_for_path.each { |template|
+ name = File.basename(template)
+ if !seen_names.include?(name)
+ seen_names << name
+ templates << template
+ end
+ }
+ }
+ templates
+ end
+
+ # Returns the first path to the given template in our
+ # app/Engine 'chain'.
+ def find_template_root_for(template)
+ all_paths = get_all_templates_for_action(template)
+ if all_paths.empty?
+ return template_path
+ else
+ return File.dirname(all_paths[0])
+ end
+ end
+
+ # JGA - changed here to include the method name that we
+ # are interested in, so that we can re-locate the proper
+ # template root
+ def initialize_template_class(assigns, method_name)
+ engine_template = find_template_root_for(method_name)
+ action_view_class = Class.new(ActionView::Base).send(:include, master_helper_module)
+ action_view_class.new(engine_template, assigns, self)
+ end
+
+
+ end
+end
--- /dev/null
+require 'fileutils'
+
+module ::ActionView
+ class Base
+
+ private
+ def full_template_path(template_path, extension)
+
+ unless Engines.disable_app_views_loading
+ # If the template exists in the normal application directory,
+ # return that path
+ default_template = "#{@base_path}/#{template_path}.#{extension}"
+ return default_template if File.exist?(default_template)
+ end
+
+ # Otherwise, check in the engines to see if the template can be found there.
+ # Load this in order so that more recently started Engines will take priority.
+ Engines.each(:precidence_order) do |engine|
+ site_specific_path = File.join(engine.root, 'app', 'views',
+ template_path.to_s + '.' + extension.to_s)
+ return site_specific_path if File.exist?(site_specific_path)
+ end
+
+ # If it cannot be found anywhere, return the default path, where the
+ # user *should* have put it.
+ return "#{@base_path}/#{template_path}.#{extension}"
+ end
+ end
+
+
+ # add methods to handle including javascripts and stylesheets
+ module Helpers
+ module AssetTagHelper
+ # Returns a stylesheet link tag to the named stylesheet(s) for the given
+ # engine. A stylesheet with the same name as the engine is included automatically.
+ # If other names are supplied, those stylesheets from within the same engine
+ # will be linked too.
+ #
+ # engine_stylesheet "my_engine" =>
+ # <link href="/engine_files/my_engine/stylesheets/my_engine.css" media="screen" rel="Stylesheet" type="text/css" />
+ #
+ # engine_stylesheet "my_engine", "another_file", "one_more" =>
+ # <link href="/engine_files/my_engine/stylesheets/my_engine.css" media="screen" rel="Stylesheet" type="text/css" />
+ # <link href="/engine_files/my_engine/stylesheets/another_file.css" media="screen" rel="Stylesheet" type="text/css" />
+ # <link href="/engine_files/my_engine/stylesheets/one_more.css" media="screen" rel="Stylesheet" type="text/css" />
+ #
+ # Any options supplied as a Hash as the last argument will be processed as in
+ # stylesheet_link_tag.
+ #
+ def engine_stylesheet(engine_name, *sources)
+ stylesheet_link_tag(*convert_public_sources(engine_name, :stylesheet, sources))
+ end
+
+ # Returns a javascript link tag to the named stylesheet(s) for the given
+ # engine. A javascript file with the same name as the engine is included automatically.
+ # If other names are supplied, those javascript from within the same engine
+ # will be linked too.
+ #
+ # engine_javascript "my_engine" =>
+ # <script type="text/javascript" src="/engine_files/my_engine/javascripts/my_engine.js"></script>
+ #
+ # engine_javascript "my_engine", "another_file", "one_more" =>
+ # <script type="text/javascript" src="/engine_files/my_engine/javascripts/my_engine.js"></script>
+ # <script type="text/javascript" src="/engine_files/my_engine/javascripts/another_file.js"></script>
+ # <script type="text/javascript" src="/engine_files/my_engine/javascripts/one_more.js"></script>
+ #
+ # Any options supplied as a Hash as the last argument will be processed as in
+ # javascript_include_tag.
+ #
+ def engine_javascript(engine_name, *sources)
+ javascript_include_tag(*convert_public_sources(engine_name, :javascript, sources))
+ end
+
+ # Returns a image tag based on the parameters passed to it
+ # Required option is option[:engine] in order to correctly idenfity the correct engine location
+ #
+ # engine_image 'rails-engines.png', :engine => 'my_engine', :alt => 'My Engine' =>
+ # <img src="/engine_files/my_engine/images/rails-engines.png" alt="My Engine />
+ #
+ # Any options supplied as a Hash as the last argument will be processed as in
+ # image_tag.
+ #
+ def engine_image(src, options = {})
+ return if !src
+
+ image_src = engine_image_src(src, options)
+
+ options.delete(:engine)
+
+ image_tag(image_src, options)
+ end
+
+ # Alias for engine_image
+ def engine_image_tag(src, options = {})
+ engine_image(src, options)
+ end
+
+ # Returns a path to the image stored within the engine_files
+ # Required option is option[:engine] in order to correctly idenfity the correct engine location
+ #
+ # engine_image_src 'rails-engines.png', :engine => 'my_engine' =>
+ # "/engine_files/my_engine/images/rails-engines.png"
+ #
+ def engine_image_src(src, options = {})
+ File.join(Engines.get(options[:engine].to_sym).public_dir, 'images', src)
+ end
+
+ private
+ # convert the engine public file sources into actual public paths
+ # type:
+ # :stylesheet
+ # :javascript
+ def convert_public_sources(engine_name, type, sources)
+ options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
+ new_sources = []
+
+ case type
+ when :javascript
+ type_dir = "javascripts"
+ ext = "js"
+ when :stylesheet
+ type_dir = "stylesheets"
+ ext = "css"
+ end
+
+ engine = Engines.get(engine_name)
+
+ default = "#{engine.public_dir}/#{type_dir}/#{engine_name}"
+ if defined?(RAILS_ROOT) && File.exists?(File.join(RAILS_ROOT, "public", "#{default}.#{ext}"))
+ new_sources << default
+ end
+
+ sources.each { |name|
+ new_sources << "#{engine.public_dir}/#{type_dir}/#{name}"
+ }
+
+ new_sources << options
+ end
+ end
+ end
+end
--- /dev/null
+module ::ActiveRecord
+ class Base
+ class << self
+
+ # NOTE: Currently the Migrations system will ALWAYS wrap given table names
+ # in the prefix/suffix, so any table name set via config(:table_name), for instnace
+ # will always get wrapped in the process of migration. For this reason, whatever
+ # value you give to the config will be wrapped when set_table_name is used in the
+ # model.
+
+ def wrapped_table_name(name)
+ table_name_prefix + name + table_name_suffix
+ end
+ end
+ end
+end
+
+# Set ActiveRecord to ignore the engine_schema_info table by default
+unless Rails::VERSION::STRING =~ /^1\.0\./
+ ::ActiveRecord::SchemaDumper.ignore_tables << 'engine_schema_info'
+end
\ No newline at end of file
--- /dev/null
+module ::Dependencies
+
+ # we're going to intercept the require_or_load method; lets
+ # make an alias for the current method so we can use it as the basis
+ # for loading from engines.
+ alias :rails_pre_engines_require_or_load :require_or_load
+
+ def require_or_load(file_name)
+ if Engines.config(:edge)
+ rails_edge_require_or_load(file_name)
+
+ elsif Rails::VERSION::STRING =~ /^1.1/
+ # otherwise, assume we're on trunk (1.1 at the moment)
+ rails_1_1_require_or_load(file_name)
+
+ elsif Rails::VERSION::STRING =~ /^1.0/
+ # use the old dependency load method
+ rails_1_0_require_or_load(file_name)
+ end
+ end
+
+ def rails_edge_require_or_load(file_name)
+ rails_1_1_require_or_load(file_name)
+ end
+
+ def rails_1_1_require_or_load(file_name)
+ Engines.log.debug("Engines 1.1 require_or_load: #{file_name}")
+
+ found = false
+
+ # try and load the engine code first
+ # can't use model, as there's nothing in the name to indicate that the file is a 'model' file
+ # rather than a library or anything else.
+ ['controller', 'helper'].each do |type|
+ # if we recognise this type
+ # (this regexp splits out the module/filename from any instances of app/#{type}, so that
+ # modules are still respected.)
+ if file_name =~ /^(.*app\/#{type}s\/)?(.*_#{type})(\.rb)?$/
+
+ # ... go through the active engines from first started to last, so that
+ # code with a high precidence (started later) will override lower precidence
+ # implementations
+ Engines.each(:load_order) do |engine|
+
+ engine_file_name = File.expand_path(File.join(engine.root, 'app', "#{type}s", $2))
+ #engine_file_name = $1 if engine_file_name =~ /^(.*)\.rb$/
+ Engines.log.debug("checking engine '#{engine.name}' for '#{engine_file_name}'")
+ if File.exist?("#{engine_file_name}.rb")
+ Engines.log.debug("==> loading from engine '#{engine.name}'")
+ rails_pre_engines_require_or_load(engine_file_name)
+ found = true
+ end
+ end
+ end
+ end
+
+ # finally, load any application-specific controller classes using the 'proper'
+ # rails load mechanism, EXCEPT when we're testing engines and could load this file
+ # from an engine
+ rails_pre_engines_require_or_load(file_name) unless Engines.disable_app_code_mixing && found
+ end
+
+ def rails_1_0_require_or_load(file_name)
+ file_name = $1 if file_name =~ /^(.*)\.rb$/
+
+ Engines.log.debug "Engines 1.0.0 require_or_load '#{file_name}'"
+
+ # if the file_name ends in "_controller" or "_controller.rb", strip all
+ # path information out of it except for module context, and load it. Ditto
+ # for helpers.
+ found = if file_name =~ /_controller(.rb)?$/
+ require_engine_files(file_name, 'controller')
+ elsif file_name =~ /_helper(.rb)?$/ # any other files we can do this with?
+ require_engine_files(file_name, 'helper')
+ end
+
+ # finally, load any application-specific controller classes using the 'proper'
+ # rails load mechanism, EXCEPT when we're testing engines and could load this file
+ # from an engine
+ Engines.log.debug("--> loading from application: '#{file_name}'")
+ rails_pre_engines_require_or_load(file_name) unless Engines.disable_app_code_mixing && found
+ Engines.log.debug("--> Done loading.")
+ end
+
+ # Load the given file (which should be a path to be matched from the root of each
+ # engine) from all active engines which have that file.
+ # NOTE! this method automagically strips file_name up to and including the first
+ # instance of '/app/controller'. This should correspond to the app/controller folder
+ # under RAILS_ROOT. However, if you have your Rails application residing under a
+ # path which includes /app/controller anyway, such as:
+ #
+ # /home/username/app/controller/my_web_application # == RAILS_ROOT
+ #
+ # then you might have trouble. Sorry, just please don't have your web application
+ # running under a path like that.
+ def require_engine_files(file_name, type='')
+ found = false
+ Engines.log.debug "requiring #{type} file '#{file_name}'"
+ processed_file_name = file_name.gsub(/[\w\W\/\.]*app\/#{type}s\//, '')
+ Engines.log.debug "--> rewrote to '#{processed_file_name}'"
+ Engines.each(:load_order) do |engine|
+ engine_file_name = File.join(engine.root, 'app', "#{type}s", processed_file_name)
+ engine_file_name += '.rb' unless ! load? || engine_file_name[-3..-1] == '.rb'
+ Engines.log.debug "--> checking '#{engine.name}' for #{engine_file_name}"
+ if File.exist?(engine_file_name) ||
+ (engine_file_name[-3..-1] != '.rb' && File.exist?(engine_file_name + '.rb'))
+ Engines.log.debug "--> found, loading from engine '#{engine.name}'"
+ rails_pre_engines_require_or_load(engine_file_name)
+ found = true
+ end
+ end
+ found
+ end
+end
+
+
+# We only need to deal with LoadingModules in Rails 1.0.0
+if Rails::VERSION::STRING =~ /^1.0/ && !Engines.config(:edge)
+ module ::Dependencies
+ class RootLoadingModule < LoadingModule
+ # hack to allow adding to the load paths within the Rails Dependencies mechanism.
+ # this allows Engine classes to be unloaded and loaded along with standard
+ # Rails application classes.
+ def add_path(path)
+ @load_paths << (path.kind_of?(ConstantLoadPath) ? path : ConstantLoadPath.new(path))
+ end
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+#require 'active_record/connection_adapters/abstract/schema_statements'
+
+module ::ActiveRecord::ConnectionAdapters::SchemaStatements
+ alias :old_initialize_schema_information :initialize_schema_information
+ def initialize_schema_information
+ # create the normal schema stuff
+ old_initialize_schema_information
+
+ # create the engines schema stuff.
+ begin
+ execute "CREATE TABLE #{engine_schema_info_table_name} (engine_name #{type_to_sql(:string)}, version #{type_to_sql(:integer)})"
+ rescue ActiveRecord::StatementInvalid
+ # Schema has been initialized
+ end
+ end
+
+ def engine_schema_info_table_name
+ ActiveRecord::Base.wrapped_table_name "engine_schema_info"
+ end
+end
+
+
+require 'breakpoint'
+module ::Engines
+ class EngineMigrator < ActiveRecord::Migrator
+
+ # We need to be able to set the 'current' engine being migrated.
+ cattr_accessor :current_engine
+
+ class << self
+
+ def schema_info_table_name
+ ActiveRecord::Base.wrapped_table_name "engine_schema_info"
+ end
+
+ def current_version
+ result = ActiveRecord::Base.connection.select_one("SELECT version FROM #{schema_info_table_name} WHERE engine_name = '#{current_engine.name}'")
+ if result
+ result["version"].to_i
+ else
+ # There probably isn't an entry for this engine in the migration info table.
+ # We need to create that entry, and set the version to 0
+ ActiveRecord::Base.connection.execute("INSERT INTO #{schema_info_table_name} (version, engine_name) VALUES (0,'#{current_engine.name}')")
+ 0
+ end
+ end
+ end
+
+ def set_schema_version(version)
+ ActiveRecord::Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{down? ? version.to_i - 1 : version.to_i} WHERE engine_name = '#{self.current_engine.name}'")
+ end
+ end
+end
--- /dev/null
+module ActionController
+ module Routing
+
+ class << self
+ # This holds the global list of valid controller paths
+ attr_accessor :controller_paths
+ end
+
+ class ControllerComponent
+ class << self
+ protected
+ def safe_load_paths #:nodoc:
+ if defined?(RAILS_ROOT)
+ paths = $LOAD_PATH.select do |base|
+ base = File.expand_path(base)
+ # Check that the path matches one of the allowed paths in controller_paths
+ base.match(/^#{ActionController::Routing.controller_paths.map { |p| File.expand_path(p) } * '|'}/)
+ end
+ Engines.log.debug "Engines safe_load_paths: #{paths.inspect}"
+ paths
+ else
+ $LOAD_PATH
+ end
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
--- /dev/null
+#--
+# Add these methods to the top-level module so that they are available in all
+# modules, etc
+#++
+class ::Module
+ # Defines a constant within a module/class ONLY if that constant does
+ # not already exist.
+ #
+ # This can be used to implement defaults in plugins/engines/libraries, e.g.
+ # if a plugin module exists:
+ # module MyPlugin
+ # default_constant :MyDefault, "the_default_value"
+ # end
+ #
+ # then developers can override this default by defining that constant at
+ # some point *before* the module/plugin gets loaded (such as environment.rb)
+ def default_constant(name, value)
+ if !(name.is_a?(String) or name.is_a?(Symbol))
+ raise "Cannot use a #{name.class.name} ['#{name}'] object as a constant name"
+ end
+ if !self.const_defined?(name)
+ self.class_eval("#{name} = #{value.inspect}")
+ end
+ end
+
+ # A mechanism for defining configuration of Modules. With this
+ # mechanism, default values for configuration can be provided within shareable
+ # code, and the end user can customise the configuration without having to
+ # provide all values.
+ #
+ # Example:
+ #
+ # module MyModule
+ # config :param_one, "some value"
+ # config :param_two, 12345
+ # end
+ #
+ # Those values can now be accessed by the following method
+ #
+ # MyModule.config :param_one
+ # => "some value"
+ # MyModule.config :param_two
+ # => 12345
+ #
+ # ... or, if you have overrriden the method 'config'
+ #
+ # MyModule::CONFIG[:param_one]
+ # => "some value"
+ # MyModule::CONFIG[:param_two]
+ # => 12345
+ #
+ # Once a value is stored in the configuration, it will not be altered
+ # by subsequent assignments, unless a special flag is given:
+ #
+ # (later on in your code, most likely in another file)
+ # module MyModule
+ # config :param_one, "another value"
+ # config :param_two, 98765, :force
+ # end
+ #
+ # The configuration is now:
+ #
+ # MyModule.config :param_one
+ # => "some value" # not changed
+ # MyModule.config :param_two
+ # => 98765
+ #
+ # Configuration values can also be given as a Hash:
+ #
+ # MyModule.config :param1 => 'value1', :param2 => 'value2'
+ #
+ # Setting of these values can also be forced:
+ #
+ # MyModule.config :param1 => 'value3', :param2 => 'value4', :force => true
+ #
+ # A value of anything other than false or nil given for the :force key will
+ # result in the new values *always* being set.
+ def config(*args)
+
+ raise "config expects at least one argument" if args.empty?
+
+ # extract the arguments
+ if args[0].is_a?(Hash)
+ override = args[0][:force]
+ args[0].delete(:force)
+ args[0].each { |key, value| _handle_config(key, value, override)}
+ else
+ _handle_config(*args)
+ end
+ end
+
+ private
+ # Actually set the config values
+ def _handle_config(name, value=nil, override=false)
+ if !self.const_defined?("CONFIG")
+ self.class_eval("CONFIG = {}")
+ end
+
+ if value != nil
+ if override or self::CONFIG[name] == nil
+ self::CONFIG[name] = value
+ end
+ else
+ # if we pass an array of config keys to config(),
+ # get the array of values back
+ if name.is_a? Array
+ name.map { |c| self::CONFIG[c] }
+ else
+ self::CONFIG[name]
+ end
+ end
+ end
+end
--- /dev/null
+# The Engines testing extensions enable developers to load fixtures into specific
+# tables irrespective of the name of the fixtures file. This work is heavily based on
+# patches made by Duane Johnson (canadaduane), viewable at
+# http://dev.rubyonrails.org/ticket/1911
+#
+# Engine developers should supply fixture files in the <engine>/test/fixtures directory
+# as normal. Within their tests, they should load the fixtures using the 'fixture' command
+# (rather than the normal 'fixtures' command). For example:
+#
+# class UserTest < Test::Unit::TestCase
+# fixture :users, :table_name => LoginEngine.config(:user_table), :class_name => "User"
+#
+# ...
+#
+# This will ensure that the fixtures/users.yml file will get loaded into the correct
+# table, and will use the correct model object class.
+
+
+
+# A FixtureGroup is a set of fixtures identified by a name. Normally, this is the name of the
+# corresponding fixture filename. For example, when you declare the use of fixtures in a
+# TestUnit class, like so:
+# fixtures :users
+# you are creating a FixtureGroup whose name is 'users', and whose defaults are set such that the
+# +class_name+, +file_name+ and +table_name+ are guessed from the FixtureGroup's name.
+class FixtureGroup
+ attr_accessor :table_name, :class_name, :connection
+ attr_reader :group_name, :file_name
+
+ def initialize(file_name, optional_names = {})
+ self.file_name = file_name
+ self.group_name = optional_names[:group_name] || file_name
+ if optional_names[:table_name]
+ self.table_name = optional_names[:table_name]
+ self.class_name = optional_names[:class_name] || Inflector.classify(@table_name.to_s.gsub('.','_'))
+ elsif optional_names[:class_name]
+ self.class_name = optional_names[:class_name]
+ if Object.const_defined?(@class_name)
+ model_class = Object.const_get(@class_name)
+ self.table_name = ActiveRecord::Base.table_name_prefix + model_class.table_name + ActiveRecord::Base.table_name_suffix
+ end
+ end
+
+ # In case either :table_name or :class_name was not set:
+ self.table_name ||= ActiveRecord::Base.table_name_prefix + @group_name.to_s + ActiveRecord::Base.table_name_suffix
+ self.class_name ||= Inflector.classify(@table_name.to_s.gsub('.','_'))
+ end
+
+ def file_name=(name)
+ @file_name = name.to_s
+ end
+
+ def group_name=(name)
+ @group_name = name.to_sym
+ end
+
+ def class_file_name
+ Inflector.underscore(@class_name)
+ end
+
+ # Instantiate an array of FixtureGroup objects from an array of strings (table_names)
+ def self.array_from_names(names)
+ names.collect { |n| FixtureGroup.new(n) }
+ end
+
+ def hash
+ @group_name.hash
+ end
+
+ def eql?(other)
+ @group_name.eql? other.group_name
+ end
+end
+
+class Fixtures < YAML::Omap
+
+ def self.instantiate_fixtures(object, fixture_group_name, fixtures, load_instances=true)
+ old_logger_level = ActiveRecord::Base.logger.level
+ ActiveRecord::Base.logger.level = Logger::ERROR
+
+ # table_name.to_s.gsub('.','_') replaced by 'fixture_group_name'
+ object.instance_variable_set "@#{fixture_group_name}", fixtures
+ if load_instances
+ ActiveRecord::Base.silence do
+ fixtures.each do |name, fixture|
+ begin
+ if model = fixture.find
+ object.instance_variable_set "@#{name}", model
+ end
+ rescue FixtureClassNotFound
+ # Let's hope the developer has included it himself
+ end
+ end
+ end
+ end
+
+ ActiveRecord::Base.logger.level = old_logger_level
+ end
+
+ # this doesn't really need to be overridden...
+ def self.instantiate_all_loaded_fixtures(object, load_instances=true)
+ all_loaded_fixtures.each do |fixture_group_name, fixtures|
+ Fixtures.instantiate_fixtures(object, fixture_group_name, fixtures, load_instances)
+ end
+ end
+
+ def self.create_fixtures(fixtures_directory, *fixture_groups)
+ connection = block_given? ? yield : ActiveRecord::Base.connection
+ fixture_groups.flatten!
+
+ # Backwards compatibility: Allow an array of table names to be passed in, but just use them
+ # to create an array of FixtureGroup objects
+ if not fixture_groups.empty? and fixture_groups.first.is_a?(String)
+ fixture_groups = FixtureGroup.array_from_names(fixture_groups)
+ end
+
+ ActiveRecord::Base.silence do
+ fixtures_map = {}
+ fixtures = fixture_groups.map do |group|
+ fixtures_map[group.group_name] = Fixtures.new(connection, fixtures_directory, group)
+ end
+ # Make sure all refs to all_loaded_fixtures use group_name as hash index, not table_name
+ all_loaded_fixtures.merge! fixtures_map
+
+ connection.transaction do
+ fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
+ fixtures.each { |fixture| fixture.insert_fixtures }
+
+ # Cap primary key sequences to max(pk).
+ if connection.respond_to?(:reset_pk_sequence!)
+ fixture_groups.each do |fg|
+ connection.reset_pk_sequence!(fg.table_name)
+ end
+ end
+ end
+
+ return fixtures.size > 1 ? fixtures : fixtures.first
+ end
+ end
+
+
+ attr_accessor :connection, :fixtures_directory, :file_filter
+ attr_accessor :fixture_group
+
+ def initialize(connection, fixtures_directory, fixture_group, file_filter = DEFAULT_FILTER_RE)
+ @connection, @fixtures_directory = connection, fixtures_directory
+ @fixture_group = fixture_group
+ @file_filter = file_filter
+ read_fixture_files
+ end
+
+ def delete_existing_fixtures
+ @connection.delete "DELETE FROM #{@fixture_group.table_name}", 'Fixture Delete'
+ end
+
+ def insert_fixtures
+ values.each do |fixture|
+ @connection.execute "INSERT INTO #{@fixture_group.table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
+ end
+ end
+
+ private
+ def read_fixture_files
+ if File.file?(yaml_file_path)
+ read_yaml_fixture_files
+ elsif File.file?(csv_file_path)
+ read_csv_fixture_files
+ elsif File.file?(deprecated_yaml_file_path)
+ raise Fixture::FormatError, ".yml extension required: rename #{deprecated_yaml_file_path} to #{yaml_file_path}"
+ elsif File.directory?(single_file_fixtures_path)
+ read_standard_fixture_files
+ else
+ raise Fixture::FixtureError, "Couldn't find a yaml, csv or standard file to load at #{@fixtures_directory} (#{@fixture_group.file_name})."
+ end
+ end
+
+ def read_yaml_fixture_files
+ # YAML fixtures
+ begin
+ if yaml = YAML::load(erb_render(IO.read(yaml_file_path)))
+ yaml = yaml.value if yaml.respond_to?(:type_id) and yaml.respond_to?(:value)
+ yaml.each do |name, data|
+ self[name] = Fixture.new(data, fixture_group.class_name)
+ end
+ end
+ rescue Exception=>boom
+ raise Fixture::FormatError, "a YAML error occured parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{boom.class}: #{boom}"
+ end
+ end
+
+ def read_csv_fixture_files
+ # CSV fixtures
+ reader = CSV::Reader.create(erb_render(IO.read(csv_file_path)))
+ header = reader.shift
+ i = 0
+ reader.each do |row|
+ data = {}
+ row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
+ self["#{@fixture_group.class_file_name}_#{i+=1}"]= Fixture.new(data, @fixture_group.class_name)
+ end
+ end
+
+ def read_standard_fixture_files
+ # Standard fixtures
+ path = File.join(@fixtures_directory, @fixture_group.file_name)
+ Dir.entries(path).each do |file|
+ path = File.join(@fixtures_directory, @fixture_group.file_name, file)
+ if File.file?(path) and file !~ @file_filter
+ self[file] = Fixture.new(path, @fixture_group.class_name)
+ end
+ end
+ end
+
+ def yaml_file_path
+ fixture_path_with_extension ".yml"
+ end
+
+ def deprecated_yaml_file_path
+ fixture_path_with_extension ".yaml"
+ end
+
+ def csv_file_path
+ fixture_path_with_extension ".csv"
+ end
+
+ def single_file_fixtures_path
+ fixture_path_with_extension ""
+ end
+
+ def fixture_path_with_extension(ext)
+ File.join(@fixtures_directory, @fixture_group.file_name + ext)
+ end
+
+ def erb_render(fixture_content)
+ ERB.new(fixture_content).result
+ end
+
+end
+
+module Test #:nodoc:
+ module Unit #:nodoc:
+ class TestCase #:nodoc:
+ cattr_accessor :fixtures_directory
+ class_inheritable_accessor :fixture_groups
+ class_inheritable_accessor :fixture_table_names
+ class_inheritable_accessor :use_transactional_fixtures
+ class_inheritable_accessor :use_instantiated_fixtures # true, false, or :no_instances
+ class_inheritable_accessor :pre_loaded_fixtures
+
+ self.fixture_groups = []
+ self.use_transactional_fixtures = false
+ self.use_instantiated_fixtures = true
+ self.pre_loaded_fixtures = false
+
+ @@already_loaded_fixtures = {}
+
+ # Backwards compatibility
+ def self.fixture_path=(path); self.fixtures_directory = path; end
+ def self.fixture_path; self.fixtures_directory; end
+ def fixture_group_names; fixture_groups.collect { |g| g.group_name }; end
+ def fixture_table_names; fixture_group_names; end
+
+ def self.fixture(file_name, options = {})
+ self.fixture_groups |= [FixtureGroup.new(file_name, options)]
+ require_fixture_classes
+ setup_fixture_accessors
+ end
+
+ def self.fixtures(*file_names)
+ self.fixture_groups |= FixtureGroup.array_from_names(file_names.flatten)
+ require_fixture_classes
+ setup_fixture_accessors
+ end
+
+ def self.require_fixture_classes(fixture_groups_override = nil)
+ (fixture_groups_override || fixture_groups).each do |group|
+ begin
+ require group.class_file_name
+ rescue LoadError
+ # Let's hope the developer has included it himself
+ end
+ end
+ end
+
+ def self.setup_fixture_accessors(fixture_groups_override=nil)
+ (fixture_groups_override || fixture_groups).each do |group|
+ define_method(group.group_name) do |fixture, *optionals|
+ force_reload = optionals.shift
+ @fixture_cache[group.group_name] ||= Hash.new
+ @fixture_cache[group.group_name][fixture] = nil if force_reload
+ @fixture_cache[group.group_name][fixture] ||= @loaded_fixtures[group.group_name][fixture.to_s].find
+ end
+ end
+ end
+
+ private
+ def load_fixtures
+ @loaded_fixtures = {}
+ fixtures = Fixtures.create_fixtures(fixtures_directory, fixture_groups)
+ unless fixtures.nil?
+ if fixtures.instance_of?(Fixtures)
+ @loaded_fixtures[fixtures.fixture_group.group_name] = fixtures
+ else
+ fixtures.each { |f| @loaded_fixtures[f.fixture_group.group_name] = f }
+ end
+ end
+ end
+
+ def instantiate_fixtures
+ if pre_loaded_fixtures
+ raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
+ unless @@required_fixture_classes
+ groups = Fixtures.all_loaded_fixtures.values.collect { |f| f.group_name }
+ self.class.require_fixture_classes groups
+ @@required_fixture_classes = true
+ end
+ Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
+ else
+ raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
+ @loaded_fixtures.each do |fixture_group_name, fixtures|
+ Fixtures.instantiate_fixtures(self, fixture_group_name, fixtures, load_instances?)
+ end
+ end
+ end
+ end
+ end
+end
--- /dev/null
+# Old-style engines rake tasks.
+# NOTE: THESE ARE DEPRICATED! PLEASE USE THE NEW STYLE!
+
+task :engine_info => "engines:info"
+task :engine_migrate => "db:migrate:engines"
+task :enginedoc => "doc:engines"
+task :load_plugin_fixtures => "db:fixtures:engines:load"
\ No newline at end of file
--- /dev/null
+module Engines
+ module RakeTasks
+ def self.all_engines
+ # An engine is informally defined as any subdirectory in vendor/plugins
+ # which ends in '_engine', '_bundle', or contains an 'init_engine.rb' file.
+ engine_base_dirs = ['vendor/plugins']
+ # The engine root may be different; if possible try to include
+ # those directories too
+ if Engines.const_defined?(:CONFIG)
+ engine_base_dirs << Engines::CONFIG[:root]
+ end
+ engine_base_dirs.map! {|d| [d + '/*_engine/*',
+ d + '/*_bundle/*',
+ d + '/*/init_engine.rb']}.flatten!
+ engine_dirs = FileList.new(*engine_base_dirs)
+ engine_dirs.map do |engine|
+ File.basename(File.dirname(engine))
+ end.uniq
+ end
+ end
+end
+
+
+namespace :engines do
+ desc "Display version information about active engines"
+ task :info => :environment do
+ if ENV["ENGINE"]
+ e = Engines.get(ENV["ENGINE"])
+ header = "Details for engine '#{e.name}':"
+ puts header
+ puts "-" * header.length
+ puts "Version: #{e.version}"
+ puts "Details: #{e.info}"
+ else
+ puts "Engines plugin: #{Engines.version}"
+ Engines.each do |e|
+ puts "#{e.name}: #{e.version}"
+ end
+ end
+ end
+end
+
+namespace :db do
+ namespace :fixtures do
+ namespace :engines do
+
+ desc "Load plugin/engine fixtures into the current environment's database."
+ task :load => :environment do
+ require 'active_record/fixtures'
+ ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
+ plugin = ENV['ENGINE'] || '*'
+ Dir.glob(File.join(RAILS_ROOT, 'vendor', 'plugins', plugin, 'test', 'fixtures', '*.yml')).each do |fixture_file|
+ Fixtures.create_fixtures(File.dirname(fixture_file), File.basename(fixture_file, '.*'))
+ end
+ end
+
+ end
+ end
+
+
+ namespace :migrate do
+
+ desc "Migrate all engines. Target specific version with VERSION=x, specific engine with ENGINE=x"
+ task :engines => :environment do
+ engines_to_migrate = ENV["ENGINE"] ? [Engines.get(ENV["ENGINE"])].compact : Engines.active
+ if engines_to_migrate.empty?
+ puts "Couldn't find an engine called '#{ENV["ENGINE"]}'"
+ else
+ if ENV["VERSION"] && !ENV["ENGINE"]
+ # ignore the VERSION, since it makes no sense in this context; we wouldn't
+ # want to revert ALL engines to the same version because of a misttype
+ puts "Ignoring the given version (#{ENV["VERSION"]})."
+ puts "To control individual engine versions, use the ENGINE=<engine> argument"
+ else
+ engines_to_migrate.each do |engine|
+ Engines::EngineMigrator.current_engine = engine
+ migration_directory = File.join(engine.root, 'db', 'migrate')
+ if File.exist?(migration_directory)
+ puts "Migrating engine '#{engine.name}'"
+ Engines::EngineMigrator.migrate(migration_directory, ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
+ else
+ puts "The db/migrate directory for engine '#{engine.name}' appears to be missing."
+ puts "Should be: #{migration_directory}"
+ end
+ end
+ if ActiveRecord::Base.schema_format == :ruby && !engines_to_migrate.empty?
+ Rake::Task[:db_schema_dump].invoke
+ end
+ end
+ end
+ end
+
+ namespace :engines do
+ Engines::RakeTasks.all_engines.each do |engine_name|
+ desc "Migrate the '#{engine_name}' engine. Target specific version with VERSION=x"
+ task engine_name => :environment do
+ ENV['ENGINE'] = engine_name; Rake::Task['db:migrate:engines'].invoke
+ end
+ end
+ end
+
+ end
+end
+
+
+# this is just a rip-off from the plugin stuff in railties/lib/tasks/documentation.rake,
+# because the default plugindoc stuff doesn't support subdirectories like <engine>/app or
+# <engine>/component.
+namespace :doc do
+
+ desc "Generate documation for all installed engines"
+ task :engines => Engines::RakeTasks.all_engines.map {|engine| "doc:engines:#{engine}"}
+
+ namespace :engines do
+ # Define doc tasks for each engine
+ Engines::RakeTasks.all_engines.each do |engine_name|
+ desc "Generation documentation for the '#{engine_name}' engine"
+ task engine_name => :environment do
+ engine_base = "vendor/plugins/#{engine_name}"
+ options = []
+ files = Rake::FileList.new
+ options << "-o doc/plugins/#{engine_name}"
+ options << "--title '#{engine_name.titlecase} Documentation'"
+ options << '--line-numbers --inline-source'
+ options << '--all' #Â include protected methods
+ options << '-T html'
+
+ files.include("#{engine_base}/lib/**/*.rb")
+ files.include("#{engine_base}/app/**/*.rb") # include the app directory
+ files.include("#{engine_base}/components/**/*.rb") # include the components directory
+ if File.exists?("#{engine_base}/README")
+ files.include("#{engine_base}/README")
+ options << "--main '#{engine_base}/README'"
+ end
+ files.include("#{engine_base}/CHANGELOG") if File.exists?("#{engine_base}/CHANGELOG")
+
+ options << files.to_s
+
+ sh %(rdoc #{options * ' '})
+ end
+ end
+ end
+end
+
+namespace :test do
+ desc "Run the engine tests in vendor/plugins/**/test (or specify with ENGINE=name)"
+ # NOTE: we're using the Rails 1.0 non-namespaced task here, just to maintain
+ # compatibility with Rails 1.0
+ # TODO: make this work with Engines.config(:root)
+
+ namespace :engines do
+ Engines::RakeTasks.all_engines.each do |engine_name|
+ desc "Run the engine tests for '#{engine_name}'"
+ Rake::TestTask.new(engine_name => :prepare_test_database) do |t|
+ t.libs << 'test'
+ t.pattern = "vendor/plugins/#{engine_name}/test/**/*_test.rb"
+ t.verbose = true
+ end
+ end
+ end
+
+ Rake::TestTask.new(:engines => [:warn_about_multiple_engines_testing, :prepare_test_database]) do |t|
+ t.libs << "test"
+ engines = ENV['ENGINE'] || '**'
+ t.pattern = "vendor/plugins/#{engines}/test/**/*_test.rb"
+ t.verbose = true
+ end
+
+ task :warn_about_multiple_engines_testing do
+ puts %{-~============== A Moste Polite Warninge ==================~-
+You may experience issues testing multiple engines at once.
+Please test engines individual for the moment.
+-~===============( ... as you were ... )===================~-
+}
+ end
+end
\ No newline at end of file
--- /dev/null
+ENV["RAILS_ENV"] = "test"
+require File.expand_path(File.dirname(__FILE__) + '/../../../../config/environment')
+require 'test_help'
+
+class ActionViewExtensionsTest < Test::Unit::TestCase
+ def test_stylesheet_path
+ assert true
+ end
+end
\ No newline at end of file
--- /dev/null
+ENV["RAILS_ENV"] = "test"
+require File.expand_path(File.dirname(__FILE__) + '/../../../../config/environment')
+require 'test_help'
+
+class RubyExtensionsTest < Test::Unit::TestCase
+
+ def setup
+ # create the module to be used for config testing
+ eval "module TestModule end"
+ end
+
+ def teardown
+ # remove the TestModule constant from our scope
+ self.class.class_eval { remove_const :TestModule }
+ end
+
+
+ #
+ # Module.config
+ #
+
+ def test_config_no_arguments
+ assert_raise(RuntimeError) { TestModule.config }
+ end
+
+ def test_config_array_arguments
+ TestModule.config :monkey, 123
+ assert_equal(123, TestModule.config(:monkey))
+ end
+
+ def test_config_hash_arguments
+ TestModule.config :monkey => 123, :donkey => 456
+ assert_equal(123, TestModule.config(:monkey))
+ assert_equal(456, TestModule.config(:donkey))
+ end
+
+ def test_config_can_store_hash
+ TestModule.config :hash, :key1 => 'val1', :key2 => 'val2'
+ assert_equal({:key1 => 'val1', :key2 => 'val2'}, TestModule.config(:hash))
+ end
+
+ def test_config_cant_overwrite_existing_config_values
+ TestModule.config :monkey, 123
+ assert_equal(123, TestModule.config(:monkey))
+ TestModule.config :monkey, 456
+ assert_equal(123, TestModule.config(:monkey))
+
+ TestModule.config :monkey => 456
+ assert_equal(123, TestModule.config(:monkey))
+
+ # in this case, the resulting Hash only has {:baboon => "goodbye!"} - that's Ruby, users beware.
+ TestModule.config :baboon => "hello", :baboon => "goodbye!"
+ assert_equal("goodbye!", TestModule.config(:baboon))
+ end
+
+ def test_config_force_new_value
+ TestModule.config :monkey, 123
+ TestModule.config :man, 321
+ assert_equal(123, TestModule.config(:monkey))
+ assert_equal(321, TestModule.config(:man))
+ TestModule.config :monkey, 456, :force
+ assert_equal(456, TestModule.config(:monkey))
+ TestModule.config :monkey => 456, :man => 654, :force => true
+ assert_equal(456, TestModule.config(:monkey))
+ assert_equal(654, TestModule.config(:man))
+ TestModule.config :monkey => 789, :man => 987, :force => false
+ assert_equal(456, TestModule.config(:monkey))
+ assert_equal(654, TestModule.config(:man))
+
+ TestModule.config :hash, :key1 => 'val1', :key2 => 'val2'
+ assert_equal({:key1 => 'val1', :key2 => 'val2'}, TestModule.config(:hash))
+ TestModule.config :hash => {:key1 => 'val3', :key2 => 'val4'}, :force => true
+ assert_equal({:key1 => 'val3', :key2 => 'val4'}, TestModule.config(:hash))
+ end
+
+ # this test is somewhat redundant, but it might be an idea to havbe it explictly anyway
+ def test_config_get_values
+ TestModule.config :monkey, 123
+ assert_equal(123, TestModule.config(:monkey))
+ end
+
+ def test_config_get_multiple_values
+ TestModule.config :monkey, 123
+ TestModule.config :donkey, 456
+ assert_equal([123, 456], TestModule.config([:monkey, :donkey]))
+ end
+
+
+ #
+ # Module.default_constant
+ #
+
+ def test_default_constant_set
+ TestModule.default_constant :Monkey, 123
+ assert_equal(123, TestModule::Monkey)
+ TestModule.default_constant "Hello", 456
+ assert_equal(456, TestModule::Hello)
+ end
+
+ def test_default_constant_cannot_set_again
+ TestModule.default_constant :Monkey, 789
+ assert_equal(789, TestModule::Monkey)
+ TestModule.default_constant :Monkey, 456
+ assert_equal(789, TestModule::Monkey)
+ end
+
+ def test_default_constant_bad_arguments
+ # constant names must be Captialized
+ assert_raise(NameError) { TestModule.default_constant :lowercase_name, 123 }
+
+ # constant names should be given as Strings or Symbols
+ assert_raise(RuntimeError) { TestModule.default_constant 123, 456 }
+ assert_raise(RuntimeError) { TestModule.default_constant Object.new, 456 }
+ end
+end
--- /dev/null
+= v1.0.2
+* Added version
+* Removed errant requires no longer needed (murray.steele@gmail.com, Ticket #156, Ticket #157, Ticket #158)
+# Removed documentation/rake tasks that refer the schema.rb (Ticket #155)
+# Verified cannot be assigned via URL parameters. If more security is required, users should override the signup action itself (Ticket #169)
+# Minor view/flash message cleanup
+# Authentication by token now respects primary key prefixes (Ticket #140)
+
+= v1.0.1
+ * Added CHANGELOG
+ * Changed wording for when password forgotten to 'reset', rather than 'retrieve'. (snowblink@gmail.com)
+ * Fixed new location of engines testing extensions. (lazyatom@gmail.com)
+ * Removed schema.db from Login Engine; migrations should be used instead. (snowblink@gmail.com)
+ * Updated User Controller tests to parse the user_id and email out of the URL in the email body. (snowblink@gmail.com)
+ * Ticket #89 (lazyatom@gmail.com) User creation halts the after_save callback chain.
+ * Ticket #97 (dcorbin@machturtle.com) The forgotten_password view generates invalid HTML
+ * Ticket #112 (segabor@gmail.com) Authentication system will break even on successful login
+ * Added simple email validation to the User model. (snowblink@gmail.com)
+ This should also take care of the unit test failures detailed in Ticket #114 (morris@wolfman.com)
+ * Ticket #118 (augustz@augustz.com) SVN source for login_engine not found
+ * Ticket #119 (Goynang) Unit tests for engines fail after default install
+ * Ticket #126 (lazyatom@gmail.com) Add install.rb to login engine
--- /dev/null
+= Before we start
+
+This is a Rails Engine version of the Salted Login Generator, a most excellent login system which is sufficient for most simple cases. For the most part, this code has not been altered from its generator form, with the following notable exceptions
+
+* Localization has been removed.
+* The 'welcome' page has been changed to the 'home' page
+* A few new functions have been thrown in
+* It's... uh.... a Rails Engine now ;-)
+
+However, what I'm trying to say is that 99.9999% of the credit for this should go to Joe Hosteny, Tobias Luetke (xal) and the folks that worked on the original Salted Login generator code. I've just wrapped it into something runnable with the Rails Engine system.
+
+Please also bear in mind that this is a work in progress, and things like testing are wildly up in the air... but they will fall into place very soon. And now, on with the show.
+
+
+= Installation
+
+Installing the Login Engine is fairly simple.
+
+Your options are:
+ 1. Install as a rails plugin:
+ $ script/plugin install login_engine
+ 2. Use svn:externals
+ $ svn propedit svn:externals vendor/plugins
+
+ You can choose to use the latest stable release:
+ login_engine http://svn.rails-engines.org/plugins/login_engine
+
+ Or a tagged release (recommended for releases of your code):
+ login_engine http://svn.rails-engines.org/logine_engine/tags/<TAGGED_RELEASE>
+
+There are a few configuration steps that you'll need to take to get everything running smoothly. Listed below are the changes to your application you will need to make.
+
+=== Setup your Rails application
+
+Edit your <tt>database.yml</tt>, most importantly! You might also want to move <tt>public/index.html</tt> out of the way, and set up some default routes in <tt>config/routes.rb</tt>.
+
+=== Add configuration and start engine
+
+Add the following to the bottom of environment.rb:
+
+ module LoginEngine
+ config :salt, "your-salt-here"
+ end
+
+ Engines.start :login
+
+You'll probably want to change the Salt value to something unique. You can also override any of the configuration values defined at the top of lib/user_system.rb in a similar way. Note that you don't need to start the engine with <tt>Engines.start :login_engine</tt> - instead, <tt>:login</tt> (or any name) is sufficient if the engine is a directory named <some-name>_engine.
+
+
+=== Add the filters
+
+Next, edit your <tt>app/controllers/application.rb</tt> file. The beginning of your <tt>ApplicationController</tt> should look something like this:
+
+ require 'login_engine'
+
+ class ApplicationController < ActionController::Base
+ include LoginEngine
+ helper :user
+ model :user
+
+ before_filter :login_required
+
+If you don't want ALL actions to require a login, you need to read further below to learn how to restrict only certain actions.
+
+Add the following to your ApplicationHelper:
+
+ module ApplicationHelper
+ include LoginEngine
+ end
+
+This ensures that the methods to work with users in your views are available
+
+=== Set up ActionMailer
+
+If you want to disable email functions within the Login Engine, simple set the :use_email_notification config flag to false in your environment.rb file:
+
+ module LoginEngine
+
+ # ... other options...
+ config :use_email_notification, false
+
+ end
+
+You should note that retrieving forgotten passwords automatically isn't possible when the email functions are disabled. Instead, the user is presented with a message instructing them to contact the system administrator
+
+If you wish you use email notifications and account creation verification, you must properly configure ActionMailer for your mail settings. For example, you could add the following in config/environments/development.rb (for a .Mac account, and with your own username and password, obviously):
+
+ActionMailer::Base.server_settings = {
+ :address => "smtp.mac.com",
+ :port => 25,
+ :domain => "smtp.mac.com",
+ :user_name => "<your user name here>",
+ :password => "<your password here>",
+ :authentication => :login
+}
+
+You'll need to configure it properly so that email can be sent. One of the easiest ways to test your configuration is to temporarily reraise exceptions from the signup method (so that you get the actual mailer exception string). In the rescue statement, put a single "raise" statement in. Once you've debugged any setting problems, remove that statement to get the proper flash error handling back.
+
+
+=== Create the DB schema
+
+After you have done the modifications the the ApplicationController and its helper, you can import the user model into the database. Migration information in login_engine/db/migrate/.
+
+You *MUST* check that these files aren't going to interfere with anything in your application.
+
+You can change the table name used by adding
+
+ module LoginEngine
+
+ # ... other options...
+ config :user_table, "your_table_name"
+
+ end
+
+...to the LoginEngine configuration in <tt>environment.rb</tt>. Then run from the root of your project:
+
+ rake db:migrate:engines ENGINE=login
+
+to import the schema into your database.
+
+
+== Include stylesheets
+
+If you want the default stylesheet, add the following line to your layout:
+
+ <%= engine_stylesheet 'login_engine' %>
+
+... somewhere in the <head> section of your HTML layout file.
+
+== Integrate flash messages into your layout
+
+LoginEngine does not display any flash messages in the views it contains, and thus you must display them yourself. This allows you to integrate any flash messages into your existing layout. LoginEngine adheres to the emerging flash usage standard, namely:
+
+* :warning - warning (failure) messages
+* :notice - success messages
+* :message - neutral (reminder, informational) messages
+
+This gives you the flexibility to theme the different message classes separately. In your layout you should check for and display flash[:warning], flash[:notice] and flash[:message]. For example:
+
+ <% for name in [:notice, :warning, :message] %>
+ <% if flash[name] %>
+ <%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
+ <% end %>
+ <% end %>
+
+Alternately, you could look at using the flash helper plugin (available from https://opensvn.csie.org/traccgi/flash_helper_plugin/trac.cgi/), which supports the same naming convention.
+
+
+= How to use the Login Engine
+
+Now you can go around and happily add "before_filter :login_required" to the controllers which you would like to protect.
+
+After integrating the login system with your rails application navigate to your new controller's signup method. There you can create a new account. After you are done you should have a look at your DB. Your freshly created user will be there but the password will be a sha1 hashed 40 digit mess. I find this should be the minimum of security which every page offering login & password should give its customers. Now you can move to one of those controllers which you protected with the before_filter :login_required snippet. You will automatically be re-directed to your freshly created login controller and you are asked for a password. After entering valid account data you will be taken back to the controller which you requested earlier. Simple huh?
+
+=== Protection using <tt>before_filter</tt>
+
+Adding the line <tt>before_filter :login_required</tt> to your <tt>app/controllers/application.rb</tt> file will protect *all* of your applications methods, in every controller. If you only want to control access to specific controllers, remove this line from <tt>application.rb</tt> and add it to the controllers that you want to secure.
+
+Within individual controllers you can restrict which methods the filter runs on in the usual way:
+
+ before_filter :login_required, :only => [:myaccount, :changepassword]
+ before_filter :login_required, :except => [:index]
+
+=== Protection using <tt>protect?()</tt>
+
+Alternatively, you can leave the <tt>before_filter</tt> in the global <tt>application.rb</tt> file, and control which actions are restricted in individual controllers by defining a <tt>protect?()</tt> method in that controller.
+
+For instance, in the <tt>UserController</tt> we want to allow everyone access to the 'login', 'signup' and 'forgot_password' methods (otherwise noone would be able to access our site!). So a <tt>protect?()</tt> method is defined in <tt>user_controller.rb</tt> as follows:
+
+ def protect?(action)
+ if ['login', 'signup', 'forgot_password'].include?(action)
+ return false
+ else
+ return true
+ end
+ end
+
+Of course, you can override this Engine behaviour in your application - see below.
+
+== Configuration
+
+The following configuration variables are set in lib/login_engine.rb. If you wish to override them, you should set them BEFORE calling Engines.start (it is possible to set them after, but it's simpler to just do it before. Please refer to the Engine documentation for the #config method for more information).
+
+For example, the following might appear at the bottom of /config/environment.rb:
+
+ module LoginEngine
+ config :salt, 'my salt'
+ config :app_name, 'My Great App'
+ config :app_url, 'http://www.wow-great-domain.com'
+ end
+
+ Engines.start
+
+=== Configuration Options
+
++email_from+:: The email from which registration/administration emails will appear to
+ come from. Defaults to 'webmaster@your.company'.
++admin_email+:: The email address users are prompted to contact if passwords cannot
+ be emailed. Defaults to 'webmaster@your.company'.
++app_url+:: The URL of the site sent to users for signup/forgotten passwords, etc.
+ Defaults to 'http://localhost:3000/'.
++app_name+:: The application title used in emails. Defaults to 'TestApp'.
++mail_charset+:: The charset used in emails. Defaults to 'utf-8'.
++security_token_life_hours+:: The life span of security tokens, in hours. If a security
+ token is older than this when it is used to try and authenticate
+ a user, it will be discarded. In other words, the amount of time
+ new users have between signing up and clicking the link they
+ are sent. Defaults to 24 hours.
++two_column_input+:: If true, forms created with the UserHelper#form_input method will
+ use a two-column table. Defaults to true.
++changeable_fields+:: An array of fields within the user model which the user
+ is allowed to edit. The Salted Hash Login generator documentation
+ states that you should NOT include the email field in this
+ array, although I am not sure why. Defaults to +[ 'firstname', 'lastname' ]+.
++delayed_delete+:: Set to true to allow delayed deletes (i.e., delete of record
+ doesn't happen immediately after user selects delete account,
+ but rather after some expiration of time to allow this action
+ to be reverted). Defaults to false.
++delayed_delete_days+:: The time delay used for the 'delayed_delete' feature. Defaults to
+ 7 days.
++user_table+:: The table to store User objects in. Defaults to "users" (or "user" if
+ ActiveRecord pluralization is disabled).
++use_email_notification+:: If false, no emails will be sent to the user. As a consequence,
+ users who signup are immediately verified, and they cannot request
+ forgotten passwords. Defaults to true.
++confirm_account+:: An overriding flag to control whether or not user accounts must be
+ verified by email. This overrides the +user_email_notification+ flag.
+ Defaults to true.
+
+== Overriding controllers and views
+
+The standard home page is almost certainly not what you want to present to your users. Because this login system is a Rails Engine, overriding the default behaviour couldn't be simpler. To change the RHTML template shown for the <tt>home</tt> action, simple create a new file in <tt>RAILS_ROOT/app/views/user/home.rhtml</tt> (you'll probably need to create the directory <tt>user</tt> at the same time). This new view file will be used instead of the one provided in the Login Engine. Easy!
+
+
+== Tips & Tricks
+
+How do I...
+
+ ... access the user who is currently logged in
+
+ A: You can get the user object from the session using session[:user]
+ Example:
+ Welcome <%= session[:user].name %>
+
+ You can also use the 'current_user' method provided by UserHelper:
+ Example:
+ Welcome <%= current_user.name %>
+
+
+ ... restrict access to only a few methods?
+
+ A: Use before_filters build in scoping.
+ Example:
+ before_filter :login_required, :only => [:myaccount, :changepassword]
+ before_filter :login_required, :except => [:index]
+
+ ... check if a user is logged-in in my views?
+
+ A: session[:user] will tell you. Here is an example helper which you can use to make this more pretty:
+ Example:
+ def user?
+ !session[:user].nil?
+ end
+
+ ... return a user to the page they came from before logging in?
+
+ A: The user will be send back to the last url which called the method "store_location"
+ Example:
+ User was at /articles/show/1, wants to log in.
+ in articles_controller.rb, add store_location to the show function and
+ send the user to the login form.
+ After he logs in he will be send back to /articles/show/1
+
+You can find more help at http://wiki.rubyonrails.com/rails/show/SaltedLoginGenerator
+
+== Troubleshooting
+
+One of the more common problems people have seen is that after verifying an account by following the emailed URL, they are unable to login via the normal login method since the verified field is not properly set in the user model's row in the DB.
+
+The most common cause of this problem is that the DB and session get out of sync. In particular, it always happens for me after recreating the DB if I have run the server previously. To fix the problem, remove the /tmp/ruby* session files (from wherever they are for your installation) while the server is stopped, and then restart. This usually is the cause of the problem.
+
+= Notes
+
+=== Database Schemas & Testing
+
+Currently, since not all databases appear to support structure cloning, the tests will load the entire schema into your test database, potentially blowing away any other test structures you might have. If this presents an issue for your application, comment out the line in test/test_helper.rb
+
+
+= Database Schema Details
+
+You need a database table corresponding to the User model. This is provided as a Rails Schema file, but the schema is presented below for information. Note the table type for MySQL. Whatever DB you use, it must support transactions. If it does not, the functional tests will not work properly, nor will the application in the face of failures during certain DB creates and updates.
+
+ mysql syntax:
+ CREATE TABLE users (
+ id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ login VARCHAR(80) NOT NULL,
+ salted_password VARCHAR(40) NOT NULL,
+ email VARCHAR(60) NOT NULL,
+ firstname VARCHAR(40),
+ lastname VARCHAR(40),
+ salt CHAR(40) NOT NULL,
+ verified INT default 0,
+ role VARCHAR(40) default NULL,
+ security_token CHAR(40) default NULL,
+ token_expiry DATETIME default NULL,
+ deleted INT default 0,
+ delete_after DATETIME default NULL
+ ) TYPE=InnoDB DEFAULT CHARSET=utf8;
+
+ postgres:
+ CREATE TABLE "users" (
+ id SERIAL PRIMARY KEY
+ login VARCHAR(80) NOT NULL,
+ salted_password VARCHAR(40) NOT NULL,
+ email VARCHAR(60) NOT NULL,
+ firstname VARCHAR(40),
+ lastname VARCHAR(40),
+ salt CHAR(40) NOT NULL,
+ verified INT default 0,
+ role VARCHAR(40) default NULL,
+ security_token CHAR(40) default NULL,
+ token_expiry TIMESTAMP default NULL,
+ deleted INT default 0,
+ delete_after TIMESTAMP default NULL
+ ) WITH OIDS;
+
+ sqlite:
+ CREATE TABLE 'users' (
+ id INTEGER PRIMARY KEY,
+ login VARCHAR(80) NOT NULL,
+ salted_password VARCHAR(40) NOT NULL,
+ email VARCHAR(60) NOT NULL,
+ firstname VARCHAR(40),
+ lastname VARCHAR(40),
+ salt CHAR(40) NOT NULL,
+ verified INT default 0,
+ role VARCHAR(40) default NULL,
+ security_token CHAR(40) default NULL,
+ token_expiry DATETIME default NULL,
+ deleted INT default 0,
+ delete_after DATETIME default NULL
+ );
+
+Of course your user model can have any amount of extra fields. This is just a starting point.
--- /dev/null
+class UserController < ApplicationController
+ model :user
+
+ # Override this function in your own application to define a custom home action.
+ def home
+ if user?
+ @fullname = "#{current_user.firstname} #{current_user.lastname}"
+ else
+ @fullname = "Not logged in..."
+ end # this is a bit of a hack since the home action is used to verify user
+ # keys, where noone is logged in. We should probably create a unique
+ # 'validate_key' action instead.
+ end
+
+ # The action used to log a user in. If the user was redirected to the login page
+ # by the login_required method, they should be sent back to the page they were
+ # trying to access. If not, they will be sent to "/user/home".
+ def login
+ return if generate_blank
+ @user = User.new(params[:user])
+ if session[:user] = User.authenticate(params[:user][:login], params[:user][:password])
+ session[:user].logged_in_at = Time.now
+ session[:user].save
+ flash[:notice] = 'Login successful'
+ redirect_to_stored_or_default :action => 'home'
+ else
+ @login = params[:user][:login]
+ flash.now[:warning] = 'Login unsuccessful'
+ end
+ end
+
+ # Register as a new user. Upon successful registration, the user will be sent to
+ # "/user/login" to enter their details.
+ def signup
+ return if generate_blank
+ params[:user].delete('form')
+ params[:user].delete('verified') # you CANNOT pass this as part of the request
+ @user = User.new(params[:user])
+ begin
+ User.transaction(@user) do
+ @user.new_password = true
+ unless LoginEngine.config(:use_email_notification) and LoginEngine.config(:confirm_account)
+ @user.verified = 1
+ end
+ if @user.save
+ key = @user.generate_security_token
+ url = url_for(:action => 'home', :user_id => @user.id, :key => key)
+ flash[:notice] = 'Signup successful!'
+ if LoginEngine.config(:use_email_notification) and LoginEngine.config(:confirm_account)
+ UserNotify.deliver_signup(@user, params[:user][:password], url)
+ flash[:notice] << ' Please check your registered email account to verify your account registration and continue with the login.'
+ else
+ flash[:notice] << ' Please log in.'
+ end
+ redirect_to :action => 'login'
+ end
+ end
+ rescue Exception => e
+ flash.now[:notice] = nil
+ flash.now[:warning] = 'Error creating account: confirmation email not sent'
+ logger.error "Unable to send confirmation E-Mail:"
+ logger.error e
+ end
+ end
+
+ def logout
+ session[:user] = nil
+ redirect_to :action => 'login'
+ end
+
+ def change_password
+ return if generate_filled_in
+ if do_change_password_for(@user)
+ # since sometimes we're changing the password from within another action/template...
+ #redirect_to :action => params[:back_to] if params[:back_to]
+ redirect_back_or_default :action => 'change_password'
+ end
+ end
+
+ protected
+ def do_change_password_for(user)
+ begin
+ User.transaction(user) do
+ user.change_password(params[:user][:password], params[:user][:password_confirmation])
+ if user.save
+ if LoginEngine.config(:use_email_notification)
+ UserNotify.deliver_change_password(user, params[:user][:password])
+ flash[:notice] = "Updated password emailed to #{@user.email}"
+ else
+ flash[:notice] = "Password updated."
+ end
+ return true
+ else
+ flash[:warning] = 'There was a problem saving the password. Please retry.'
+ return false
+ end
+ end
+ rescue
+ flash[:warning] = 'Password could not be changed at this time. Please retry.'
+ end
+ end
+
+ public
+
+
+ def forgot_password
+ # Always redirect if logged in
+ if user?
+ flash[:message] = 'You are currently logged in. You may change your password now.'
+ redirect_to :action => 'change_password'
+ return
+ end
+
+ # Email disabled... we are unable to provide the password
+ if !LoginEngine.config(:use_email_notification)
+ flash[:message] = "Please contact the system admin at #{LoginEngine.config(:admin_email)} to reset your password."
+ redirect_back_or_default :action => 'login'
+ return
+ end
+
+ # Render on :get and render
+ return if generate_blank
+
+ # Handle the :post
+ if params[:user][:email].empty?
+ flash.now[:warning] = 'Please enter a valid email address.'
+ elsif (user = User.find_by_email(params[:user][:email])).nil?
+ flash.now[:warning] = "We could not find a user with the email address #{params[:user][:email]}"
+ else
+ begin
+ User.transaction(user) do
+ key = user.generate_security_token
+ url = url_for(:action => 'change_password', :user_id => user.id, :key => key)
+ UserNotify.deliver_forgot_password(user, url)
+ flash[:notice] = "Instructions on resetting your password have been emailed to #{params[:user][:email]}"
+ end
+ unless user?
+ redirect_to :action => 'login'
+ return
+ end
+ redirect_back_or_default :action => 'home'
+ rescue
+ flash.now[:warning] = "Your password could not be emailed to #{params[:user][:email]}"
+ end
+ end
+ end
+
+ def edit
+ return if generate_filled_in
+ do_edit_user(@user)
+ end
+
+ protected
+ def do_edit_user(user)
+ begin
+ User.transaction(user) do
+ user.attributes = params[:user].delete_if { |k,v| not LoginEngine.config(:changeable_fields).include?(k) }
+ if user.save
+ flash[:notice] = "User details updated"
+ else
+ flash[:warning] = "Details could not be updated! Please retry."
+ end
+ end
+ rescue
+ flash.now[:warning] = "Error updating user details. Please try again later."
+ end
+ end
+
+ public
+
+ def delete
+ get_user_to_act_on
+ if do_delete_user(@user)
+ logout
+ else
+ redirect_back_or_default :action => 'home'
+ end
+ end
+
+ protected
+ def do_delete_user(user)
+ begin
+ if LoginEngine.config(:delayed_delete)
+ User.transaction(user) do
+ key = user.set_delete_after
+ if LoginEngine.config(:use_email_notification)
+ url = url_for(:action => 'restore_deleted', :user_id => user.id, :key => key)
+ UserNotify.deliver_pending_delete(user, url)
+ end
+ end
+ else
+ destroy(@user)
+ end
+ return true
+ rescue
+ if LoginEngine.config(:use_email_notification)
+ flash.now[:warning] = 'The delete instructions were not sent. Please try again later.'
+ else
+ flash.now[:notice] = 'The account has been scheduled for deletion. It will be removed in #{LoginEngine.config(:delayed_delete_days)} days.'
+ end
+ return false
+ end
+ end
+
+ public
+
+ def restore_deleted
+ get_user_to_act_on
+ @user.deleted = 0
+ if not @user.save
+ flash.now[:warning] = "The account for #{@user['login']} was not restored. Please try the link again."
+ redirect_to :action => 'login'
+ else
+ redirect_to :action => 'home'
+ end
+ end
+
+ protected
+
+ def destroy(user)
+ UserNotify.deliver_delete(user) if LoginEngine.config(:use_email_notification)
+ flash[:notice] = "The account for #{user['login']} was successfully deleted."
+ user.destroy()
+ end
+
+ def protect?(action)
+ if ['login', 'signup', 'forgot_password'].include?(action)
+ return false
+ else
+ return true
+ end
+ end
+
+ # Generate a template user for certain actions on get
+ def generate_blank
+ case request.method
+ when :get
+ @user = User.new
+ render
+ return true
+ end
+ return false
+ end
+
+ # Generate a template user for certain actions on get
+ def generate_filled_in
+ get_user_to_act_on
+ case request.method
+ when :get
+ render
+ return true
+ end
+ return false
+ end
+
+ # returns the user object this method should act upon; only really
+ # exists for other engines operating on top of this one to redefine...
+ def get_user_to_act_on
+ @user = session[:user]
+ end
+end
--- /dev/null
+module UserHelper
+
+ # Abstraction to make views a little cleaner
+ def form_input(helper_method, prompt, field_name=nil, options = {}, form_name = nil)
+ form_name = "user" if form_name.nil?
+ case helper_method.to_s
+ when 'hidden_field'
+ self.hidden_field(form_name, field_name, options)
+ when /^.*button$/
+ #prompt = l(:"#{@controller.controller_name}_#{field_name}_button")
+ <<-EOL
+ <tr><td class="button" colspan="2">
+ #{self.send(helper_method, form_name, prompt, options)}
+ </td></tr>
+ EOL
+ else
+ field = (
+ case helper_method
+ when :select
+ self.send(helper_method, form_name, field_name, options.delete('values'), options)
+ when :password_field
+ options[:value] = ""
+ self.send(helper_method, form_name, field_name, options)
+ else
+ self.send(helper_method, form_name, field_name, options)
+ end)
+# lname = "#{form_name}_#{field_name}_form"
+# prompt = l(:"#{lname}")
+ if LoginEngine.config(:two_column_input)
+<<-EOL
+ <tr class="two_columns">
+ <td class="prompt"><label>#{prompt}:</label></td>
+ <td class="value">#{field}</td>
+ </tr>
+ EOL
+ else
+<<-EOL
+ <tr><td class="prompt"><label>#{prompt}:</label></td></tr>
+ <tr><td class="value">#{field}</td></tr>
+ EOL
+ end
+ end
+ end
+
+# def button_helper(name, options = {})
+# label = l(:"#{@controller.controller_name}_#{name}_button")
+# "#{self.send(:submit_tag, label, options)}"
+# end
+
+# def link_helper(name, options = {})
+# raise ArgumentError if name.nil?
+# label = l(:"#{@controller.controller_name}_#{name}_link")
+# "#{self.send(:link_to, label, options)}"
+# end
+
+ def title_helper
+ "#{@controller.controller_class_name} #{@controller.action_name}"
+ end
+
+# def message_helper(name)
+# l(:"#{@controller.controller_name}_#{name}_message")
+# end
+
+ def start_form_tag_helper(options = {})
+ url = url_for(:action => "#{@controller.action_name}")
+ "#{self.send(:start_form_tag, url, options)}"
+ end
+
+ def attributes(hash)
+ hash.keys.inject("") { |attrs, key| attrs + %{#{key}="#{h(hash[key])}" } }
+ end
+
+ def read_only_field(form_name, field_name, html_options)
+ "<span #{attributes(html_options)}>#{instance_variable_get('@' + form_name)[field_name]}</span>"
+ end
+
+ def submit_button(form_name, prompt, html_options)
+ %{<input name="submit" type="submit" value="#{prompt}" />}
+ end
+
+ def changeable(user, field)
+ if user.new_record? or LoginEngine.config(:changeable_fields).include?(field)
+ :text_field
+ else
+ :read_only_field
+ end
+ end
+end
--- /dev/null
+class User < ActiveRecord::Base
+ include LoginEngine::AuthenticatedUser
+
+ # all logic has been moved into login_engine/lib/login_engine/authenticated_user.rb
+
+end
+
--- /dev/null
+class UserNotify < ActionMailer::Base
+ def signup(user, password, url=nil)
+ setup_email(user)
+
+ # Email header info
+ @subject += "Welcome to #{LoginEngine.config(:app_name)}!"
+
+ # Email body substitutions
+ @body["name"] = "#{user.firstname} #{user.lastname}"
+ @body["login"] = user.login
+ @body["password"] = password
+ @body["url"] = url || LoginEngine.config(:app_url).to_s
+ @body["app_name"] = LoginEngine.config(:app_name).to_s
+ end
+
+ def forgot_password(user, url=nil)
+ setup_email(user)
+
+ # Email header info
+ @subject += "Forgotten password notification"
+
+ # Email body substitutions
+ @body["name"] = "#{user.firstname} #{user.lastname}"
+ @body["login"] = user.login
+ @body["url"] = url || LoginEngine.config(:app_url).to_s
+ @body["app_name"] = LoginEngine.config(:app_name).to_s
+ end
+
+ def change_password(user, password, url=nil)
+ setup_email(user)
+
+ # Email header info
+ @subject += "Changed password notification"
+
+ # Email body substitutions
+ @body["name"] = "#{user.firstname} #{user.lastname}"
+ @body["login"] = user.login
+ @body["password"] = password
+ @body["url"] = url || LoginEngine.config(:app_url).to_s
+ @body["app_name"] = LoginEngine.config(:app_name).to_s
+ end
+
+ def pending_delete(user, url=nil)
+ setup_email(user)
+
+ # Email header info
+ @subject += "Delete user notification"
+
+ # Email body substitutions
+ @body["name"] = "#{user.firstname} #{user.lastname}"
+ @body["url"] = url || LoginEngine.config(:app_url).to_s
+ @body["app_name"] = LoginEngine.config(:app_name).to_s
+ @body["days"] = LoginEngine.config(:delayed_delete_days).to_s
+ end
+
+ def delete(user, url=nil)
+ setup_email(user)
+
+ # Email header info
+ @subject += "Delete user notification"
+
+ # Email body substitutions
+ @body["name"] = "#{user.firstname} #{user.lastname}"
+ @body["url"] = url || LoginEngine.config(:app_url).to_s
+ @body["app_name"] = LoginEngine.config(:app_name).to_s
+ end
+
+ def setup_email(user)
+ @recipients = "#{user.email}"
+ @from = LoginEngine.config(:email_from).to_s
+ @subject = "[#{LoginEngine.config(:app_name)}] "
+ @sent_on = Time.now
+ @headers['Content-Type'] = "text/plain; charset=#{LoginEngine.config(:mail_charset)}; format=flowed"
+ end
+end
--- /dev/null
+<div class="user_edit">
+ <table>
+ <%= form_input changeable(user, "firstname"), "First Name", "firstname" %>
+ <%= form_input changeable(user, "lastname"), "Last Name","lastname" %>
+ <%= form_input changeable(user, "login"), "Login ID", "login", :size => 30 %><br/>
+ <%= form_input changeable(user, "email"), "Email", "email" %>
+ <% if submit %>
+ <%= form_input :submit_button, (user.new_record? ? 'Signup' : 'Change Settings'), :class => 'two_columns' %>
+ <% end %>
+ </table>
+</div>
--- /dev/null
+<div class="user_password">
+ <table>
+ <%= form_input :password_field, "Password", "password", :size => 30 %>
+ <%= form_input :password_field, "Password Confirmation", "password_confirmation", :size => 30 %>
+ <% if submit %>
+ <%= form_input :submit_button, 'Change password' %>
+ <% end %>
+ </table>
+</div>
\ No newline at end of file
--- /dev/null
+<div title="<%= title_helper %>" class="form">
+ <h3>Change Password</h3>
+
+ <%= error_messages_for 'user' %>
+
+ <div class="form-padding">
+ <p>Enter your new password in the fields below and click 'Change Password' to have a new password sent to your email inbox.</p>
+
+ <%= start_form_tag :action => 'change_password' %>
+ <%= render_partial 'password', :user => @user, :submit => false %>
+ <div class="button-bar">
+ <%= submit_tag 'Change password' %>
+ <%= link_to 'Cancel', :action => 'home' %>
+ </div>
+ <%= end_form_tag %>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+<div title="<%= title_helper %>" class="form">
+ <h3>Edit user</h3>
+
+ <%= error_messages_for 'user' %>
+
+ <%= start_form_tag :action => 'edit' %>
+ <%= render_partial 'edit', :user => @user, :submit => true %>
+ <%= end_form_tag %>
+ <br/>
+ <%= start_form_tag :action => 'change_password' %>
+ <%= hidden_field_tag "back_to", "edit" %>
+ <%= render_partial 'password', :submit => true %>
+ <%= end_form_tag %>
+
+ <%= start_form_tag :action => 'delete' %>
+ <div class="user_delete">
+ <%= hidden_field 'user', 'form', :value => 'delete' %>
+
+ <%= form_input :submit_button, 'Delete Account' %>
+ </div>
+ <%= end_form_tag %>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+<div title="<%= title_helper %>" class="form">
+ <h3>Forgotten Password</h3>
+
+ <%= error_messages_for 'user' %>
+
+ <div class="form-padding">
+ <p>Enter your email address in the field below and click 'Reset Password' to have instructions on how to retrieve your forgotten password emailed to you.</p>
+
+ <%= start_form_tag_helper %>
+ <label>Email Address:</label> <%= text_field("user", "email", "size" => 30) %>
+
+ <div class="button-bar">
+ <%= submit_tag 'Reset Password' %>
+ <%= link_to 'Cancel', :action => 'login' %>
+ </div>
+ <%= end_form_tag %>
+ </div>
+</div>
--- /dev/null
+<div title="<%= title_helper %>" class="memo">
+ <h3>Welcome</h3>
+ <p>You are now logged into the system, <%= @fullname %>...</p>
+ <p>Since you are here it's safe to assume the application never called store_location, otherwise you would have been redirected somewhere else after a successful login.</p>
+
+ <%= link_to '« logout', :action => 'logout' %>
+</div>
--- /dev/null
+<div title="<%= title_helper %>" class="form">
+ <h3>Please Login</h3>
+
+ <div class="form-padding">
+ <%= start_form_tag :action => 'login' %>
+ <table>
+ <%= form_input :text_field, "Login ID", "login", :size => 30 %><br/>
+ <%= form_input :password_field, "Password", "password", :size => 30 %><br/>
+ </table>
+
+ <div class="button-bar">
+ <%= submit_tag 'Login' %>
+ <%= link_to 'Register for an account', :action => 'signup' %> |
+ <%= link_to 'Forgot my password', :action => 'forgot_password' %> </div>
+ <%= end_form_tag %>
+ </div>
+</div>
--- /dev/null
+<div title="<%= title_helper %>" class="memo">
+ <h3>Logoff</h3>
+
+ <p>You are now logged out of the system...</p>
+
+ <%= link_to '« login', :action => 'login' %>
+</div>
+
--- /dev/null
+<div title="<%= title_helper %>" class="form">
+ <h3>Signup</h3>
+
+ <%= error_messages_for 'user' %>
+
+ <div class="form-padding">
+ <%= start_form_tag :action => 'signup' %>
+ <%= render_partial 'edit', :user => @user, :submit => false %><br/>
+ <%= render_partial 'password', :submit => false %>
+
+ <div class="button-bar">
+ <%= submit_tag 'Signup' %>
+ <%= link_to 'Cancel', :action => 'login' %>
+ </div>
+ <%= end_form_tag %>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+Dear <%= @name %>,
+
+At your request, <%= @app_name %> has changed your password. If it was not at your request, then you should be aware that someone has access to your account and requested this change.
+
+Your new login credentials are:
+
+ login: <%= @login %>
+ password: <%= @password %>
+
+<%= @url %>
\ No newline at end of file
--- /dev/null
+Dear <%= @name %>,
+
+At your request, <%= @app_name %> has permanently deleted your account.
+
+<%= @url %>
\ No newline at end of file
--- /dev/null
+Dear <%= @name %>,
+
+At your request, <%= @app_name %> has sent you the following URL so that you may reset your password. If it was not at your request, then you should be aware that someone has entered your email address as theirs in the forgotten password section of <%= @app_name %>.
+
+Please click on the following link to go to the change password page:
+
+<a href="<%= @url%>">Click me!</a>
+
+It's advisable for you to change your password as soon as you login. It's as simple as navigating to 'Preferences' and clicking on 'Change Password'.
+
+<%= @url %>
\ No newline at end of file
--- /dev/null
+Dear <%= @name %>,
+
+At your request, <%= @app_name %> has marked your account for deletion. If it was not at your request, then you should be aware that someone has access to your account and requested this change.
+
+The following link is provided for you to restore your deleted account. If you click on this link within the next <%= @days %> days, your account will not be deleted. Otherwise, simply ignore this email and your account will be permanently deleted after that time.
+
+<a href="<%= @url%>">Click me!</a>
+
+<%= @url %>
\ No newline at end of file
--- /dev/null
+Welcome to <%= @app_name %>, <%= @name %>.
+
+Your login credentials are:
+
+ login: <%= @login %>
+ password: <%= @password %>
+
+Please click on the following link to confirm your registration:
+
+<a href="<%= @url%>">Click me!</a>
+
+<%= @url %>
--- /dev/null
+class InitialSchema < ActiveRecord::Migration
+ def self.up
+ create_table LoginEngine.config(:user_table), :force => true do |t|
+ t.column "login", :string, :limit => 80, :default => "", :null => false
+ t.column "salted_password", :string, :limit => 40, :default => "", :null => false
+ t.column "email", :string, :limit => 60, :default => "", :null => false
+ t.column "firstname", :string, :limit => 40
+ t.column "lastname", :string, :limit => 40
+ t.column "salt", :string, :limit => 40, :default => "", :null => false
+ t.column "verified", :integer, :default => 0
+ t.column "role", :string, :limit => 40
+ t.column "security_token", :string, :limit => 40
+ t.column "token_expiry", :datetime
+ t.column "created_at", :datetime
+ t.column "updated_at", :datetime
+ t.column "logged_in_at", :datetime
+ t.column "deleted", :integer, :default => 0
+ t.column "delete_after", :datetime
+ end
+ end
+
+ def self.down
+ drop_table LoginEngine.config(:user_table)
+ end
+end
--- /dev/null
+# load up all the required files we need...
+
+require 'login_engine'
+
+module LoginEngine::Version
+ Major = 1
+ Minor = 0
+ Release = 2
+end
+
+Engines.current.version = LoginEngine::Version
\ No newline at end of file
--- /dev/null
+# Install the engines plugin if it has been already
+unless File.exist?(File.dirname(__FILE__) + "/../engines")
+ Commands::Plugin.parse!(['install', 'http://svn.rails-engines.org/plugins/engines'])
+end
\ No newline at end of file
--- /dev/null
+require 'login_engine/authenticated_user'
+require 'login_engine/authenticated_system'
+
+module LoginEngine
+ include AuthenticatedSystem # re-include the helper module
+
+ #--
+ # Define the configuration values. config sets the value of the
+ # constant ONLY if it has not already been set, i.e. by the user in
+ # environment.rb
+ #++
+
+ # Source address for user emails
+ config :email_from, 'webmaster@your.company'
+
+ # Destination email for system errors
+ config :admin_email, 'webmaster@your.company'
+
+ # Sent in emails to users
+ config :app_url, 'http://localhost:3000/'
+
+ # Sent in emails to users
+ config :app_name, 'TestApp'
+
+ # Email charset
+ config :mail_charset, 'utf-8'
+
+ # Security token lifetime in hours
+ config :security_token_life_hours, 24
+
+ # Two column form input
+ config :two_column_input, true
+
+ # Add all changeable user fields to this array.
+ # They will then be able to be edited from the edit action. You
+ # should NOT include the email field in this array.
+ config :changeable_fields, [ 'firstname', 'lastname' ]
+
+ # Set to true to allow delayed deletes (i.e., delete of record
+ # doesn't happen immediately after user selects delete account,
+ # but rather after some expiration of time to allow this action
+ # to be reverted).
+ config :delayed_delete, false
+
+ # Default is one week
+ config :delayed_delete_days, 7
+
+ # the table to store user information in
+ if ActiveRecord::Base.pluralize_table_names
+ config :user_table, "users"
+ else
+ config :user_table, "user"
+ end
+
+ # controls whether or not email is used
+ config :use_email_notification, true
+
+ # Controls whether accounts must be confirmed after signing up
+ # ONLY if this and use_email_notification are both true
+ config :confirm_account, true
+
+end
--- /dev/null
+module LoginEngine
+ module AuthenticatedSystem
+
+ protected
+
+ # overwrite this if you want to restrict access to only a few actions
+ # or if you want to check if the user has the correct rights
+ # example:
+ #
+ # # only allow nonbobs
+ # def authorize?(user)
+ # user.login != "bob"
+ # end
+ def authorize?(user)
+ true
+ end
+
+ # overwrite this method if you only want to protect certain actions of the controller
+ # example:
+ #
+ # # don't protect the login and the about method
+ # def protect?(action)
+ # if ['action', 'about'].include?(action)
+ # return false
+ # else
+ # return true
+ # end
+ # end
+ def protect?(action)
+ true
+ end
+
+ # login_required filter. add
+ #
+ # before_filter :login_required
+ #
+ # if the controller should be under any rights management.
+ # for finer access control you can overwrite
+ #
+ # def authorize?(user)
+ #
+ def login_required
+ if not protect?(action_name)
+ return true
+ end
+
+ if user? and authorize?(session[:user])
+ return true
+ end
+
+ # store current location so that we can
+ # come back after the user logged in
+ store_location
+
+ # call overwriteable reaction to unauthorized access
+ access_denied
+ end
+
+ # overwrite if you want to have special behavior in case the user is not authorized
+ # to access the current operation.
+ # the default action is to redirect to the login screen
+ # example use :
+ # a popup window might just close itself for instance
+ def access_denied
+ redirect_to :controller => "/user", :action => "login"
+ end
+
+ # store current uri in the session.
+ # we can return to this location by calling return_location
+ def store_location
+ session['return-to'] = request.request_uri
+ end
+
+ # move to the last store_location call or to the passed default one
+ def redirect_to_stored_or_default(default=nil)
+ if session['return-to'].nil?
+ redirect_to default
+ else
+ redirect_to_url session['return-to']
+ session['return-to'] = nil
+ end
+ end
+
+ def redirect_back_or_default(default=nil)
+ if request.env["HTTP_REFERER"].nil?
+ redirect_to default
+ else
+ redirect_to(request.env["HTTP_REFERER"]) # same as redirect_to :back
+ end
+ end
+
+ def user?
+ # First, is the user already authenticated?
+ return true if not session[:user].nil?
+
+ # If not, is the user being authenticated by a token?
+ id = params[:user_id]
+ key = params[:key]
+ if id and key
+ session[:user] = User.authenticate_by_token(id, key)
+ return true if not session[:user].nil?
+ end
+
+ # Everything failed
+ return false
+ end
+
+ # Returns the current user from the session, if any exists
+ def current_user
+ session[:user]
+ end
+ end
+end
--- /dev/null
+require 'digest/sha1'
+
+# this model expects a certain database layout and its based on the name/login pattern.
+
+module LoginEngine
+ module AuthenticatedUser
+
+ def self.included(base)
+ base.class_eval do
+
+ # use the table name given
+ set_table_name LoginEngine.config(:user_table)
+
+ attr_accessor :new_password
+
+ validates_presence_of :login
+ validates_length_of :login, :within => 3..40
+ validates_uniqueness_of :login
+ validates_uniqueness_of :email
+ validates_format_of :email, :with => /^[^@]+@.+$/
+
+ validates_presence_of :password, :if => :validate_password?
+ validates_confirmation_of :password, :if => :validate_password?
+ validates_length_of :password, { :minimum => 5, :if => :validate_password? }
+ validates_length_of :password, { :maximum => 40, :if => :validate_password? }
+
+ protected
+
+ attr_accessor :password, :password_confirmation
+
+ after_save :falsify_new_password
+ after_validation :crypt_password
+
+ end
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+
+ def authenticate(login, pass)
+ u = find(:first, :conditions => ["login = ? AND verified = 1 AND deleted = 0", login])
+ return nil if u.nil?
+ find(:first, :conditions => ["login = ? AND salted_password = ? AND verified = 1", login, AuthenticatedUser.salted_password(u.salt, AuthenticatedUser.hashed(pass))])
+ end
+
+ def authenticate_by_token(id, token)
+ # Allow logins for deleted accounts, but only via this method (and
+ # not the regular authenticate call)
+ u = find(:first, :conditions => ["#{User.primary_key} = ? AND security_token = ?", id, token])
+ return nil if u.nil? or u.token_expired?
+ return nil if false == u.update_expiry
+ u
+ end
+
+ end
+
+
+ protected
+
+ def self.hashed(str)
+ # check if a salt has been set...
+ if LoginEngine.config(:salt) == nil
+ raise "You must define a :salt value in the configuration for the LoginEngine module."
+ end
+
+ return Digest::SHA1.hexdigest("#{LoginEngine.config(:salt)}--#{str}--}")[0..39]
+ end
+
+ def self.salted_password(salt, hashed_password)
+ hashed(salt + hashed_password)
+ end
+
+ public
+
+ # hmmm, how does this interact with the developer's own User model initialize?
+ # We would have to *insist* that the User.initialize method called 'super'
+ #
+ def initialize(attributes = nil)
+ super
+ @new_password = false
+ end
+
+ def token_expired?
+ self.security_token and self.token_expiry and (Time.now > self.token_expiry)
+ end
+
+ def update_expiry
+ write_attribute('token_expiry', [self.token_expiry, Time.at(Time.now.to_i + 600 * 1000)].min)
+ write_attribute('authenticated_by_token', true)
+ write_attribute("verified", 1)
+ update_without_callbacks
+ end
+
+ def generate_security_token(hours = nil)
+ if not hours.nil? or self.security_token.nil? or self.token_expiry.nil? or
+ (Time.now.to_i + token_lifetime / 2) >= self.token_expiry.to_i
+ return new_security_token(hours)
+ else
+ return self.security_token
+ end
+ end
+
+ def set_delete_after
+ hours = LoginEngine.config(:delayed_delete_days) * 24
+ write_attribute('deleted', 1)
+ write_attribute('delete_after', Time.at(Time.now.to_i + hours * 60 * 60))
+
+ # Generate and return a token here, so that it expires at
+ # the same time that the account deletion takes effect.
+ return generate_security_token(hours)
+ end
+
+ def change_password(pass, confirm = nil)
+ self.password = pass
+ self.password_confirmation = confirm.nil? ? pass : confirm
+ @new_password = true
+ end
+
+ protected
+
+ def validate_password?
+ @new_password
+ end
+
+
+ def crypt_password
+ if @new_password
+ write_attribute("salt", AuthenticatedUser.hashed("salt-#{Time.now}"))
+ write_attribute("salted_password", AuthenticatedUser.salted_password(salt, AuthenticatedUser.hashed(@password)))
+ end
+ end
+
+ def falsify_new_password
+ @new_password = false
+ true
+ end
+
+ def new_security_token(hours = nil)
+ write_attribute('security_token', AuthenticatedUser.hashed(self.salted_password + Time.now.to_i.to_s + rand.to_s))
+ write_attribute('token_expiry', Time.at(Time.now.to_i + token_lifetime(hours)))
+ update_without_callbacks
+ return self.security_token
+ end
+
+ def token_lifetime(hours = nil)
+ if hours.nil?
+ LoginEngine.config(:security_token_life_hours) * 60 * 60
+ else
+ hours * 60 * 60
+ end
+ end
+
+ end
+end
+
--- /dev/null
+/*
+
+ This CSS file is basically the scaffold.css file, and is only
+ included here to demonstrate using CSS files with Engines.
+
+*/
+
+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;
+}
+
+div.uploadStatus {
+ margin: 5px;
+}
+
+div.progressBar {
+ margin: 5px;
+}
+
+div.progressBar div.border {
+ background-color: #fff;
+ border: 1px solid grey;
+ width: 100%;
+}
+
+div.progressBar div.background {
+ background-color: #333;
+ height: 18px;
+ width: 0%;
+}
+
--- /dev/null
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+
+bob:
+ id: 1000001
+ login: bob
+ salted_password: b1de1d1d2aec05df2be6f02995537c1783f08490 # atest
+ salt: bf3c47e71c0bfeb6288c9b6b5e24e15256a0e407
+ email: bob@test.com
+ verified: 1
+
+existingbob:
+ id: 1000002
+ login: existingbob
+ salted_password: b1de1d1d2aec05df2be6f02995537c1783f08490 # atest
+ salt: bf3c47e71c0bfeb6288c9b6b5e24e15256a0e407
+ email: existingbob@test.com
+ verified: 1
+
+longbob:
+ id: 1000003
+ login: longbob
+ salted_password: 53427dca242488e885216a579e362ee888c3ebc1 # alongtest
+ salt: d35a9cc89af83799d9a938a74cb06a11d295aa9c
+ email: longbob@test.com
+ verified: 1
+
+deletebob1:
+ id: 1000004
+ login: deletebob1
+ salted_password: 53427dca242488e885216a579e362ee888c3ebc1 # alongtest
+ salt: d35a9cc89af83799d9a938a74cb06a11d295aa9c
+ email: deletebob1@test.com
+ verified: 1
+
+deletebob2:
+ id: 1000005
+ login: deletebob2
+ salted_password: 53427dca242488e885216a579e362ee888c3ebc1 # alongtest
+ salt: d35a9cc89af83799d9a938a74cb06a11d295aa9c
+ email: deletebob2@test.com
+ verified: 1
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+require_dependency 'user_controller'
+
+
+# Raise errors beyond the default web-based presentation
+class UserController; def rescue_action(e) raise e end; end
+
+class UserControllerTest < Test::Unit::TestCase
+
+ # load the fixture into the developer-specified table using the custom
+ # 'fixture' method.
+ fixture :users, :table_name => LoginEngine.config(:user_table), :class_name => "User"
+
+ def setup
+
+ LoginEngine::CONFIG[:salt] = "test-salt"
+
+ @controller = UserController.new
+ @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
+ @request.host = "localhost"
+ end
+
+
+
+ #==========================================================================
+ #
+ # Login/Logout
+ #
+ #==========================================================================
+
+ def test_home_without_login
+ get :home
+ assert_redirected_to :action => "login"
+ end
+
+ def test_invalid_login
+ post :login, :user => { :login => "bob", :password => "wrong_password" }
+ assert_response :success
+
+ assert_session_has_no :user
+ assert_template "login"
+ end
+
+ def test_login
+ @request.session['return-to'] = "/bogus/location"
+
+ post :login, :user => { :login => "bob", :password => "atest" }
+
+ assert_response 302 # redirect
+ assert_session_has :user
+ assert_equal users(:bob), session[:user]
+
+ assert_redirect_url "http://#{@request.host}/bogus/location"
+ end
+
+ def test_login_logoff
+
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ get :logout
+ assert_session_has_no :user
+
+ end
+
+
+ #==========================================================================
+ #
+ # Signup
+ #
+ #==========================================================================
+
+ def test_signup
+ LoginEngine::CONFIG[:use_email_notification] = true
+
+ ActionMailer::Base.deliveries = []
+
+ @request.session['return-to'] = "/bogus/location"
+
+ assert_equal 5, User.count
+ post :signup, :user => { :login => "newbob", :password => "newpassword", :password_confirmation => "newpassword", :email => "newbob@test.com" }
+ assert_session_has_no :user
+
+ assert_redirect_url(@controller.url_for(:action => "login"))
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ mail = ActionMailer::Base.deliveries[0]
+ assert_equal "newbob@test.com", mail.to_addrs[0].to_s
+ assert_match /login:\s+\w+\n/, mail.encoded
+ assert_match /password:\s+\w+\n/, mail.encoded
+ #mail.encoded =~ /user_id=(.*?)&key=(.*?)"/
+ user_id = /user_id=(\d+)/.match(mail.encoded)[1]
+ key = /key=([a-z0-9]+)/.match(mail.encoded)[1]
+
+ assert_not_nil user_id
+ assert_not_nil key
+
+ user = User.find_by_email("newbob@test.com")
+ assert_not_nil user
+ assert_equal 0, user.verified
+
+ # First past the expiration.
+ Time.advance_by_days = 1
+ get :home, :user_id => "#{user_id}", :key => "#{key}"
+ Time.advance_by_days = 0
+ user = User.find_by_email("newbob@test.com")
+ assert_equal 0, user.verified
+
+ # Then a bogus key.
+ get :home, :user_id => "#{user_id}", :key => "boguskey"
+ user = User.find_by_email("newbob@test.com")
+ assert_equal 0, user.verified
+
+ # Now the real one.
+ get :home, :user_id => "#{user_id}", :key => "#{key}"
+ user = User.find_by_email("newbob@test.com")
+ assert_equal 1, user.verified
+
+ post :login, :user => { :login => "newbob", :password => "newpassword" }
+ assert_session_has :user
+ get :logout
+
+ end
+
+ def test_signup_bad_password
+ LoginEngine::CONFIG[:use_email_notification] = true
+ ActionMailer::Base.deliveries = []
+
+ @request.session['return-to'] = "/bogus/location"
+ post :signup, :user => { :login => "newbob", :password => "bad", :password_confirmation => "bad", :email => "newbob@test.com" }
+ assert_session_has_no :user
+ assert_invalid_column_on_record "user", "password"
+ assert_success
+ assert_equal 0, ActionMailer::Base.deliveries.size
+ end
+
+ def test_signup_bad_email
+ LoginEngine::CONFIG[:use_email_notification] = true
+ ActionMailer::Base.deliveries = []
+
+ @request.session['return-to'] = "/bogus/location"
+
+ ActionMailer::Base.inject_one_error = true
+ post :signup, :user => { :login => "newbob", :password => "newpassword", :password_confirmation => "newpassword", :email => "newbob@test.com" }
+ assert_session_has_no :user
+ assert_equal 0, ActionMailer::Base.deliveries.size
+ end
+
+ def test_signup_without_email
+ LoginEngine::CONFIG[:use_email_notification] = false
+
+ @request.session['return-to'] = "/bogus/location"
+
+ post :signup, :user => { :login => "newbob", :password => "newpassword", :password_confirmation => "newpassword", :email => "newbob@test.com" }
+
+ assert_redirect_url(@controller.url_for(:action => "login"))
+ assert_session_has_no :user
+ assert_match /Signup successful/, flash[:notice]
+
+ assert_not_nil User.find_by_login("newbob")
+
+ user = User.find_by_email("newbob@test.com")
+ assert_not_nil user
+
+ post :login, :user => { :login => "newbob", :password => "newpassword" }
+ assert_session_has :user
+ get :logout
+ end
+
+ def test_signup_bad_details
+ @request.session['return-to'] = "/bogus/location"
+
+ # mismatched password
+ post :signup, :user => { :login => "newbob", :password => "newpassword", :password_confirmation => "wrong" }
+ assert_invalid_column_on_record "user", "password"
+ assert_success
+
+ # login not long enough
+ post :signup, :user => { :login => "yo", :password => "newpassword", :password_confirmation => "newpassword" }
+ assert_invalid_column_on_record "user", "login"
+ assert_success
+
+ # both
+ post :signup, :user => { :login => "yo", :password => "newpassword", :password_confirmation => "wrong" }
+ assert_invalid_column_on_record "user", ["login", "password"]
+ assert_success
+
+ # existing user
+ post :signup, :user => { :login => "bob", :password => "doesnt_matter", :password_confirmation => "doesnt_matter" }
+ assert_invalid_column_on_record "user", "login"
+ assert_success
+
+ # existing email
+ post :signup, :user => { :login => "newbob", :email => "longbob@test.com", :password => "doesnt_matter", :password_confirmation => "doesnt_matter" }
+ assert_invalid_column_on_record "user", "email"
+ assert_success
+
+ end
+
+
+ #==========================================================================
+ #
+ # Edit
+ #
+ #==========================================================================
+
+ def test_edit
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ post :edit, :user => { "firstname" => "Bob", "form" => "edit" }
+ assert_equal @response.session[:user].firstname, "Bob"
+
+ post :edit, :user => { "firstname" => "", "form" => "edit" }
+ assert_equal @response.session[:user].firstname, ""
+
+ get :logout
+ end
+
+
+
+ #==========================================================================
+ #
+ # Delete
+ #
+ #==========================================================================
+
+ def test_delete
+ LoginEngine::CONFIG[:use_email_notification] = true
+ # Immediate delete
+ post :login, :user => { :login => "deletebob1", :password => "alongtest" }
+ assert_session_has :user
+
+ LoginEngine.config :delayed_delete, false, :force
+ post :delete
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ assert_session_has_no :user
+
+ # try and login in again, we should fail.
+ post :login, :user => { :login => "deletebob1", :password => "alongtest" }
+ assert_session_has_no :user
+ assert_template_has "login"
+
+
+ # Now try delayed delete
+ ActionMailer::Base.deliveries = []
+
+ post :login, :user => { :login => "deletebob2", :password => "alongtest" }
+ assert_session_has :user
+
+ LoginEngine.config :delayed_delete, true, :force
+ post :delete
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ mail = ActionMailer::Base.deliveries[0]
+ user_id = /user_id=(\d+)/.match(mail.encoded)[1]
+ key = /key=([a-z0-9]+)/.match(mail.encoded)[1]
+
+ post :restore_deleted, :user_id => "#{user_id}", "key" => "badkey"
+ assert_session_has_no :user
+
+ # Advance the time past the delete date
+ Time.advance_by_days = LoginEngine.config :delayed_delete_days
+ post :restore_deleted, :user_id => "#{user_id}", "key" => "#{key}"
+ assert_session_has_no :user
+ Time.advance_by_days = 0
+
+ post :restore_deleted, :user_id => "#{user_id}", "key" => "#{key}"
+ assert_session_has :user
+ end
+
+ def test_delete_without_email
+ LoginEngine::CONFIG[:use_email_notification] = false
+ ActionMailer::Base.deliveries = []
+
+ # Immediate delete
+ post :login, :user => { :login => "deletebob1", :password => "alongtest" }
+ assert_session_has :user
+
+ LoginEngine.config :delayed_delete, false, :force
+ post :delete
+ assert_session_has_no :user
+ assert_nil User.find_by_login("deletebob1")
+
+ # try and login in again, we should fail.
+ post :login, :user => { :login => "deletebob1", :password => "alongtest" }
+ assert_session_has_no :user
+ assert_template_has "login"
+
+
+ # Now try delayed delete
+ ActionMailer::Base.deliveries = []
+
+ post :login, :user => { :login => "deletebob2", :password => "alongtest" }
+ assert_session_has :user
+
+ # delayed delete is not really relevant currently without email.
+ LoginEngine.config :delayed_delete, true, :force
+ post :delete
+ assert_equal 1, User.find_by_login("deletebob2").deleted
+ end
+
+
+
+ #==========================================================================
+ #
+ # Change Password
+ #
+ #==========================================================================
+
+ def test_change_valid_password
+
+ LoginEngine::CONFIG[:use_email_notification] = true
+
+ ActionMailer::Base.deliveries = []
+
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ post :change_password, :user => { :password => "changed_password", :password_confirmation => "changed_password" }
+
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ mail = ActionMailer::Base.deliveries[0]
+ assert_equal "bob@test.com", mail.to_addrs[0].to_s
+ assert_match /login:\s+\w+\n/, mail.encoded
+ assert_match /password:\s+\w+\n/, mail.encoded
+
+ post :login, :user => { :login => "bob", :password => "changed_password" }
+ assert_session_has :user
+ post :change_password, :user => { :password => "atest", :password_confirmation => "atest" }
+ get :logout
+
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ get :logout
+ end
+
+ def test_change_valid_password_without_email
+
+ LoginEngine::CONFIG[:use_email_notification] = false
+
+ ActionMailer::Base.deliveries = []
+
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ post :change_password, :user => { :password => "changed_password", :password_confirmation => "changed_password" }
+
+ assert_redirected_to :action => "change_password"
+
+ post :login, :user => { :login => "bob", :password => "changed_password" }
+ assert_session_has :user
+ post :change_password, :user => { :password => "atest", :password_confirmation => "atest" }
+ get :logout
+
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ get :logout
+ end
+
+ def test_change_short_password
+ LoginEngine::CONFIG[:use_email_notification] = true
+ ActionMailer::Base.deliveries = []
+
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ post :change_password, :user => { :password => "bad", :password_confirmation => "bad" }
+ assert_invalid_column_on_record "user", "password"
+ assert_success
+ assert_equal 0, ActionMailer::Base.deliveries.size
+
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ get :logout
+ end
+
+ def test_change_short_password_without_email
+ LoginEngine::CONFIG[:use_email_notification] = false
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ post :change_password, :user => { :password => "bad", :password_confirmation => "bad" }
+ assert_invalid_column_on_record "user", "password"
+ assert_success
+
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ get :logout
+ end
+
+
+ def test_change_password_with_bad_email
+ LoginEngine::CONFIG[:use_email_notification] = true
+ ActionMailer::Base.deliveries = []
+
+ # log in
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ # change the password, but the email delivery will fail
+ ActionMailer::Base.inject_one_error = true
+ post :change_password, :user => { :password => "changed_password", :password_confirmation => "changed_password" }
+ assert_equal 0, ActionMailer::Base.deliveries.size
+ assert_match /Password could not be changed/, flash[:warning]
+
+ # logout
+ get :logout
+ assert_session_has_no :user
+
+ # ensure we can log in with our original password
+ # TODO: WHY DOES THIS FAIL!! It looks like the transaction stuff in UserController#change_password isn't actually rolling back changes.
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ get :logout
+ end
+
+
+
+
+ #==========================================================================
+ #
+ # Forgot Password
+ #
+ #==========================================================================
+
+ def test_forgot_password
+ LoginEngine::CONFIG[:use_email_notification] = true
+
+ do_forgot_password(false, false, false)
+ do_forgot_password(false, false, true)
+ do_forgot_password(true, false, false)
+ do_forgot_password(false, true, false)
+ end
+
+ def do_forgot_password(bad_address, bad_email, logged_in)
+ ActionMailer::Base.deliveries = []
+
+ if logged_in
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+ end
+
+ @request.session['return-to'] = "/bogus/location"
+ if not bad_address and not bad_email
+ post :forgot_password, :user => { :email => "bob@test.com" }
+ password = "anewpassword"
+ if logged_in
+ assert_equal 0, ActionMailer::Base.deliveries.size
+ assert_redirect_url(@controller.url_for(:action => "change_password"))
+ post :change_password, :user => { :password => "#{password}", :password_confirmation => "#{password}" }
+ else
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ mail = ActionMailer::Base.deliveries[0]
+ assert_equal "bob@test.com", mail.to_addrs[0].to_s
+ user_id = /user_id=(\d+)/.match(mail.encoded)[1]
+ key = /key=([a-z0-9]+)/.match(mail.encoded)[1]
+ post :change_password, :user => { :password => "#{password}", :password_confirmation => "#{password}"}, :user_id => "#{user_id}", :key => "#{key}"
+ assert_session_has :user
+ get :logout
+ end
+ elsif bad_address
+ post :forgot_password, :user => { :email => "bademail@test.com" }
+ assert_equal 0, ActionMailer::Base.deliveries.size
+ elsif bad_email
+ ActionMailer::Base.inject_one_error = true
+ post :forgot_password, :user => { :email => "bob@test.com" }
+ assert_equal 0, ActionMailer::Base.deliveries.size
+ else
+ # Invalid test case
+ assert false
+ end
+
+ if not bad_address and not bad_email
+ if logged_in
+ get :logout
+ else
+ assert_redirect_url(@controller.url_for(:action => "login"))
+ end
+ post :login, :user => { :login => "bob", :password => "#{password}" }
+ else
+ # Okay, make sure the database did not get changed
+ if logged_in
+ get :logout
+ end
+ post :login, :user => { :login => "bob", :password => "atest" }
+ end
+
+ assert_session_has :user
+
+ # Put the old settings back
+ if not bad_address and not bad_email
+ post :change_password, :user => { :password => "atest", :password_confirmation => "atest" }
+ end
+
+ get :logout
+ end
+
+ def test_forgot_password_without_email_and_logged_in
+ LoginEngine::CONFIG[:use_email_notification] = false
+
+ post :login, :user => { :login => "bob", :password => "atest" }
+ assert_session_has :user
+
+ @request.session['return-to'] = "/bogus/location"
+ post :forgot_password, :user => { :email => "bob@test.com" }
+ password = "anewpassword"
+ assert_redirect_url(@controller.url_for(:action => "change_password"))
+ post :change_password, :user => { :password => "#{password}", :password_confirmation => "#{password}" }
+
+ get :logout
+
+ post :login, :user => { :login => "bob", :password => "#{password}" }
+
+ assert_session_has :user
+
+ get :logout
+ end
+
+ def forgot_password_without_email_and_not_logged_in
+ LoginEngine::CONFIG[:use_email_notification] = false
+
+ @request.session['return-to'] = "/bogus/location"
+ post :forgot_password, :user => { :email => "bob@test.com" }
+ password = "anewpassword"
+
+ # wothout email, you can't retrieve your forgotten password...
+ assert_match /Please contact the system admin/, flash[:message]
+ assert_session_has_no :user
+
+ assert_redirect_url "http://#{@request.host}/bogus/location"
+ end
+end
--- /dev/null
+ActionMailer::Base.class_eval {
+ @@inject_one_error = false
+ cattr_accessor :inject_one_error
+
+ private
+ def perform_delivery_test(mail)
+ if inject_one_error
+ ActionMailer::Base::inject_one_error = false
+ raise "Failed to send email" if raise_delivery_errors
+ else
+ deliveries << mail
+ end
+ end
+}
--- /dev/null
+require 'time'
+
+Time.class_eval {
+ if !respond_to? :now_old # somehow this is getting defined many times.
+ @@advance_by_days = 0
+ cattr_accessor :advance_by_days
+
+ class << Time
+ alias now_old now
+ def now
+ if Time.advance_by_days != 0
+ return Time.at(now_old.to_i + Time.advance_by_days * 60 * 60 * 24 + 1)
+ else
+ now_old
+ end
+ end
+ end
+ end
+}
--- /dev/null
+require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper') # the default rails helper
+
+# ensure that the Engines testing enhancements are loaded.
+require File.join(Engines.config(:root), "engines", "lib", "engines", "testing_extensions")
+
+require File.dirname(__FILE__) + '/mocks/time'
+require File.dirname(__FILE__) + '/mocks/mail'
+
+# set up the fixtures location
+Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
+$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+class UserTest < Test::Unit::TestCase
+
+ # load the fixture into the developer-specified table using the custom
+ # 'fixture' method.
+ fixture :users, :table_name => LoginEngine.config(:user_table), :class_name => "User"
+
+ def setup
+ LoginEngine::CONFIG[:salt] = "test-salt"
+ end
+
+ def test_auth
+ assert_equal users(:bob), User.authenticate("bob", "atest")
+ assert_nil User.authenticate("nonbob", "atest")
+ end
+
+
+ def test_passwordchange
+
+ users(:longbob).change_password("nonbobpasswd")
+ users(:longbob).save
+ assert_equal users(:longbob), User.authenticate("longbob", "nonbobpasswd")
+ assert_nil User.authenticate("longbob", "alongtest")
+ users(:longbob).change_password("alongtest")
+ users(:longbob).save
+ assert_equal users(:longbob), User.authenticate("longbob", "alongtest")
+ assert_nil User.authenticate("longbob", "nonbobpasswd")
+
+ end
+
+ def test_disallowed_passwords
+
+ u = User.new
+ u.login = "nonbob"
+ u.email = "bobs@email.com"
+
+ u.change_password("tiny")
+ assert !u.save
+ assert u.errors.invalid?('password')
+
+ u.change_password("hugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehugehuge")
+ assert !u.save
+ assert u.errors.invalid?('password')
+
+ u.change_password("")
+ assert !u.save
+ assert u.errors.invalid?('password')
+
+ u.change_password("bobs_secure_password")
+ assert u.save
+ assert u.errors.empty?
+
+ end
+
+ def test_bad_logins
+
+ u = User.new
+ u.change_password("bobs_secure_password")
+ u.email = "bobs@email.com"
+
+ u.login = "x"
+ assert !u.save
+ assert u.errors.invalid?('login')
+
+ u.login = "hugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhugebobhug"
+ assert !u.save
+ assert u.errors.invalid?('login')
+
+ u.login = ""
+ assert !u.save
+ assert u.errors.invalid?('login')
+
+ u.login = "okbob"
+ assert u.save
+ assert u.errors.empty?
+
+ end
+
+
+ def test_collision
+ u = User.new
+ u.login = "existingbob"
+ u.change_password("bobs_secure_password")
+ assert !u.save
+ end
+
+
+ def test_create
+ u = User.new
+ u.login = "nonexistingbob"
+ u.change_password("bobs_secure_password")
+ u.email = "bobs@email.com"
+
+ assert u.save
+
+ end
+
+ def test_email_should_be_nominally_valid
+ u = User.new
+ u.login = "email_test"
+ u.change_password("email_test_password")
+
+ assert !u.save
+ assert u.errors.invalid?('email')
+
+ u.email = "invalid_email"
+ assert !u.save
+ assert u.errors.invalid?('email')
+
+ u.email = "valid@email.com"
+ assert u.save
+ end
+
+end