Browse files

first commit

  • Loading branch information...
0 parents commit b47843c189aa060aa1644958ebddbb119f4e6898 @pjhyett committed Mar 13, 2010
Showing with 3,857 additions and 0 deletions.
  1. +202 −0 Rakefile
  2. +37 −0 app/controllers/account_controller.rb
  3. +6 −0 app/controllers/application.rb
  4. +11 −0 app/controllers/days_controller.rb
  5. +7 −0 app/controllers/euro_controller.rb
  6. +60 −0 app/controllers/thoughts_controller.rb
  7. +2 −0 app/helpers/account_helper.rb
  8. +8 −0 app/helpers/application_helper.rb
  9. +2 −0 app/helpers/days_helper.rb
  10. +2 −0 app/helpers/euro_helper.rb
  11. +2 −0 app/helpers/thoughts_helper.rb
  12. +3 −0 app/models/day.rb
  13. +6 −0 app/models/thought.rb
  14. +60 −0 app/models/user.rb
  15. +22 −0 app/views/account/login.rhtml
  16. +17 −0 app/views/account/signup.rhtml
  17. +12 −0 app/views/days/_thoughts.rhtml
  18. +6 −0 app/views/days/index.rhtml
  19. +1 −0 app/views/days/show.rhtml
  20. +17 −0 app/views/layouts/application.rhtml
  21. +6 −0 app/views/thoughts/_form.rhtml
  22. +9 −0 app/views/thoughts/edit.rhtml
  23. +22 −0 app/views/thoughts/list.rhtml
  24. +8 −0 app/views/thoughts/new.rhtml
  25. +23 −0 app/views/thoughts/rss.rxml
  26. +23 −0 config/database.yml
  27. +87 −0 config/environment.rb
  28. +14 −0 config/environments/development.rb
  29. +8 −0 config/environments/production.rb
  30. +17 −0 config/environments/test.rb
  31. +31 −0 config/routes.rb
  32. +22 −0 db/thoughts.sql
  33. 0 doc/README_FOR_APP
  34. +87 −0 lib/login_system.rb
  35. +32 −0 public/.htaccess
  36. +8 −0 public/404.html
  37. +8 −0 public/500.html
  38. +24 −0 public/dispatch.fcgi
  39. +10 −0 public/dispatch.rb
  40. 0 public/favicon.ico
  41. BIN public/images/blockquote.gif
  42. BIN public/images/divider.gif
  43. BIN public/images/name.gif
  44. BIN public/images/permalink.gif
  45. BIN public/images/uploads/dynamite.jpg
  46. BIN public/images/uploads/eiffel.jpg
  47. BIN public/images/uploads/einstein_ar.jpg
  48. BIN public/images/uploads/scooter.jpg
  49. +446 −0 public/javascripts/controls.js
  50. +537 −0 public/javascripts/dragdrop.js
  51. +612 −0 public/javascripts/effects.js
  52. +1,038 −0 public/javascripts/prototype.js
  53. +86 −0 public/stylesheets/thoughts.css
  54. +19 −0 script/benchmarker
  55. +4 −0 script/breakpointer
  56. +23 −0 script/console
  57. +7 −0 script/destroy
  58. +7 −0 script/generate
  59. +34 −0 script/profiler
  60. +29 −0 script/runner
  61. +49 −0 script/server
  62. +18 −0 test/functional/euro_controller_test.rb
  63. +26 −0 test/test_helper.rb
202 Rakefile
@@ -0,0 +1,202 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+$VERBOSE = nil
+TEST_CHANGES_SINCE = Time.now - 600
+
+desc "Run all the tests on a fresh test database"
+task :default => [ :test_units, :test_functional ]
+
+
+desc 'Require application environment.'
+task :environment do
+ unless defined? RAILS_ROOT
+ require File.dirname(__FILE__) + '/config/environment'
+ end
+end
+
+desc "Generate API documentation, show coding stats"
+task :doc => [ :appdoc, :stats ]
+
+
+# Look up tests for recently modified sources.
+def recent_tests(source_pattern, test_path, touched_since = 10.minutes.ago)
+ FileList[source_pattern].map do |path|
+ if File.mtime(path) > touched_since
+ test = "#{test_path}/#{File.basename(path, '.rb')}_test.rb"
+ test if File.exists?(test)
+ end
+ end.compact
+end
+
+desc 'Test recent changes.'
+Rake::TestTask.new(:recent => [ :clone_structure_to_test ]) do |t|
+ since = TEST_CHANGES_SINCE
+ touched = FileList['test/**/*_test.rb'].select { |path| File.mtime(path) > since } +
+ recent_tests('app/models/*.rb', 'test/unit', since) +
+ recent_tests('app/controllers/*.rb', 'test/functional', since)
+
+ t.libs << 'test'
+ t.verbose = true
+ t.test_files = touched.uniq
+end
+task :test_recent => [ :clone_structure_to_test ]
+
+desc "Run the unit tests in test/unit"
+Rake::TestTask.new("test_units") { |t|
+ t.libs << "test"
+ t.pattern = 'test/unit/**/*_test.rb'
+ t.verbose = true
+}
+task :test_units => [ :clone_structure_to_test ]
+
+desc "Run the functional tests in test/functional"
+Rake::TestTask.new("test_functional") { |t|
+ t.libs << "test"
+ t.pattern = 'test/functional/**/*_test.rb'
+ t.verbose = true
+}
+task :test_functional => [ :clone_structure_to_test ]
+
+desc "Generate documentation for the application"
+Rake::RDocTask.new("appdoc") { |rdoc|
+ rdoc.rdoc_dir = 'doc/app'
+ rdoc.title = "Rails Application Documentation"
+ rdoc.options << '--line-numbers --inline-source'
+ rdoc.rdoc_files.include('doc/README_FOR_APP')
+ rdoc.rdoc_files.include('app/**/*.rb')
+}
+
+desc "Generate documentation for the Rails framework"
+Rake::RDocTask.new("apidoc") { |rdoc|
+ rdoc.rdoc_dir = 'doc/api'
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
+ rdoc.title = "Rails Framework Documentation"
+ rdoc.options << '--line-numbers --inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('CHANGELOG')
+ rdoc.rdoc_files.include('vendor/rails/railties/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/rails/railties/MIT-LICENSE')
+ rdoc.rdoc_files.include('vendor/rails/activerecord/README')
+ rdoc.rdoc_files.include('vendor/rails/activerecord/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/rails/activerecord/lib/active_record/**/*.rb')
+ rdoc.rdoc_files.exclude('vendor/rails/activerecord/lib/active_record/vendor/*')
+ rdoc.rdoc_files.include('vendor/rails/actionpack/README')
+ rdoc.rdoc_files.include('vendor/rails/actionpack/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/rails/actionpack/lib/action_controller/**/*.rb')
+ rdoc.rdoc_files.include('vendor/rails/actionpack/lib/action_view/**/*.rb')
+ rdoc.rdoc_files.include('vendor/rails/actionmailer/README')
+ rdoc.rdoc_files.include('vendor/rails/actionmailer/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/rails/actionmailer/lib/action_mailer/base.rb')
+ rdoc.rdoc_files.include('vendor/rails/actionwebservice/README')
+ rdoc.rdoc_files.include('vendor/rails/actionwebservice/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service.rb')
+ rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/*.rb')
+ rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/api/*.rb')
+ rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/client/*.rb')
+ rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/container/*.rb')
+ rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/dispatcher/*.rb')
+ rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/protocol/*.rb')
+ rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/support/*.rb')
+ rdoc.rdoc_files.include('vendor/rails/activesupport/README')
+ rdoc.rdoc_files.include('vendor/rails/activesupport/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/rails/activesupport/lib/active_support/**/*.rb')
+}
+
+desc "Report code statistics (KLOCs, etc) from the application"
+task :stats => [ :environment ] do
+ require 'code_statistics'
+ CodeStatistics.new(
+ ["Helpers", "app/helpers"],
+ ["Controllers", "app/controllers"],
+ ["APIs", "app/apis"],
+ ["Components", "components"],
+ ["Functionals", "test/functional"],
+ ["Models", "app/models"],
+ ["Units", "test/unit"]
+ ).to_s
+end
+
+desc "Recreate the test databases from the development structure"
+task :clone_structure_to_test => [ :db_structure_dump, :purge_test_database ] do
+ abcs = ActiveRecord::Base.configurations
+ case abcs["test"]["adapter"]
+ when "mysql"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0')
+ IO.readlines("db/#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |table|
+ ActiveRecord::Base.connection.execute(table)
+ end
+ when "postgresql"
+ ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"]
+ ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"]
+ ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"]
+ `psql -U "#{abcs["test"]["username"]}" -f db/#{RAILS_ENV}_structure.sql #{abcs["test"]["database"]}`
+ when "sqlite", "sqlite3"
+ `#{abcs[RAILS_ENV]["adapter"]} #{abcs["test"]["dbfile"]} < db/#{RAILS_ENV}_structure.sql`
+ when "sqlserver"
+ `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql`
+ else
+ raise "Unknown database adapter '#{abcs["test"]["adapter"]}'"
+ end
+end
+
+desc "Dump the database structure to a SQL file"
+task :db_structure_dump => :environment do
+ abcs = ActiveRecord::Base.configurations
+ case abcs[RAILS_ENV]["adapter"]
+ when "mysql"
+ ActiveRecord::Base.establish_connection(abcs[RAILS_ENV])
+ File.open("db/#{RAILS_ENV}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump }
+ when "postgresql"
+ ENV['PGHOST'] = abcs[RAILS_ENV]["host"] if abcs[RAILS_ENV]["host"]
+ ENV['PGPORT'] = abcs[RAILS_ENV]["port"].to_s if abcs[RAILS_ENV]["port"]
+ ENV['PGPASSWORD'] = abcs[RAILS_ENV]["password"].to_s if abcs[RAILS_ENV]["password"]
+ `pg_dump -U "#{abcs[RAILS_ENV]["username"]}" -s -x -O -f db/#{RAILS_ENV}_structure.sql #{abcs[RAILS_ENV]["database"]}`
+ when "sqlite", "sqlite3"
+ `#{abcs[RAILS_ENV]["adapter"]} #{abcs[RAILS_ENV]["dbfile"]} .schema > db/#{RAILS_ENV}_structure.sql`
+ when "sqlserver"
+ `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /f db\\#{RAILS_ENV}_structure.sql /q /A /r`
+ `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /F db\ /q /A /r`
+ else
+ raise "Unknown database adapter '#{abcs["test"]["adapter"]}'"
+ end
+end
+
+desc "Empty the test database"
+task :purge_test_database => :environment do
+ abcs = ActiveRecord::Base.configurations
+ case abcs["test"]["adapter"]
+ when "mysql"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.recreate_database(abcs["test"]["database"])
+ when "postgresql"
+ ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"]
+ ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"]
+ ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"]
+ `dropdb -U "#{abcs["test"]["username"]}" #{abcs["test"]["database"]}`
+ `createdb -T template0 -U "#{abcs["test"]["username"]}" #{abcs["test"]["database"]}`
+ when "sqlite","sqlite3"
+ File.delete(abcs["test"]["dbfile"]) if File.exist?(abcs["test"]["dbfile"])
+ when "sqlserver"
+ dropfkscript = "#{abcs["test"]["host"]}.#{abcs["test"]["database"]}.DP1".gsub(/\\/,'-')
+ `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{dropfkscript}`
+ `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql`
+ else
+ raise "Unknown database adapter '#{abcs["test"]["adapter"]}'"
+ end
+end
+
+desc "Clears all *.log files in log/"
+task :clear_logs => :environment do
+ FileList["log/*.log"].each do |log_file|
+ f = File.open(log_file, "w")
+ f.close
+ end
+end
+
+desc "Migrate the database according to the migrate scripts in db/migrate (only supported on PG/MySQL). A specific version can be targetted with VERSION=x"
+task :migrate => :environment do
+ ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/db/migrate/', ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
+end
37 app/controllers/account_controller.rb
@@ -0,0 +1,37 @@
+class AccountController < ApplicationController
+
+ def login
+ case @request.method
+ when :post
+ if session[:user] = User.authenticate(@params[:user_login], @params[:user_password])
+
+ flash['notice'] = "Login successful"
+ redirect_back_or_default :controller => "thoughts", :action => "list"
+ else
+ flash.now['notice'] = "Login unsuccessful"
+
+ @login = @params[:user_login]
+ end
+ end
+ end
+
+ def signup
+ if(User.find_first.nil? || session[:user])
+ @user = User.new(@params[:user])
+
+ if @request.post? and @user.save
+ session[:user] = User.authenticate(@user.login, @params[:user][:password])
+ flash['notice'] = "Signup successful"
+ redirect_back_or_default :controller => "thoughts", :action => "list"
+ end
+ else
+ render_text "If you want to create another user, you must be logged in first"
+ end
+ end
+
+ def logout
+ session[:user] = nil
+ redirect_to :controller => "days", :action => "index"
+ end
+
+end
6 app/controllers/application.rb
@@ -0,0 +1,6 @@
+require_dependency "login_system"
+
+class ApplicationController < ActionController::Base
+ include LoginSystem
+ model :user
+end
11 app/controllers/days_controller.rb
@@ -0,0 +1,11 @@
+class DaysController < ApplicationController
+
+ def index
+ @day_pages, @days = paginate :day, :per_page => 5, :order => 'id desc'
+ end
+
+ def show
+ @day = Day.find(params[:id])
+ end
+
+end
7 app/controllers/euro_controller.rb
@@ -0,0 +1,7 @@
+class EuroController < ApplicationController
+
+ def index
+ redirect_to 'http://euro2005.thestashspot.com'
+ end
+
+end
60 app/controllers/thoughts_controller.rb
@@ -0,0 +1,60 @@
+# TODO: new/create & edit/update can be merged with @request.post?
+
+class ThoughtsController < ApplicationController
+ layout 'application', :except => "rss"
+ before_filter :login_required, :except => [:index, :rss]
+
+ def list
+ @thoughts = Thought.find(:all)
+ end
+
+ def new
+ @thought = Thought.new
+ end
+
+ def rss
+ @thoughts = Thought.find(:all, :order => 'id desc', :limit => 10)
+ end
+
+ def create
+ @thought = Thought.new(params[:thought])
+
+ # grab last day from db
+ @day = Day.find(:first, :order => "date desc")
+
+ # if no day yet or no current day, make a new one
+ if(@day.nil? || @day.date.yday < Time.now.yday)
+ @day = Day.new
+ @day.date = Time.now
+ @day.save
+ end
+ @thought.day_id = @day.id
+ @thought.user = session[:user]
+
+ if @thought.save
+ flash[:notice] = 'Thought was successfully created.'
+ redirect_to :action => 'list'
+ else
+ render :action => 'new'
+ end
+ end
+
+ def edit
+ @thought = Thought.find(params[:id])
+ end
+
+ def update
+ @thought = Thought.find(params[:id])
+ if @thought.update_attributes(params[:thought])
+ flash[:notice] = 'Thought was successfully updated.'
+ redirect_to :controller => 'days', :action => 'show', :id => @thought.day.id, :anchor => "thought-#{@thought.id}"
+ else
+ render :action => 'edit'
+ end
+ end
+
+ def destroy
+ Thought.find(params[:id]).destroy
+ redirect_to :action => 'list'
+ end
+end
2 app/helpers/account_helper.rb
@@ -0,0 +1,2 @@
+module AccountHelper
+end
8 app/helpers/application_helper.rb
@@ -0,0 +1,8 @@
+# Methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+
+ def user?
+ !session[:user].nil?
+ end
+
+end
2 app/helpers/days_helper.rb
@@ -0,0 +1,2 @@
+module DaysHelper
+end
2 app/helpers/euro_helper.rb
@@ -0,0 +1,2 @@
+module EuroHelper
+end
2 app/helpers/thoughts_helper.rb
@@ -0,0 +1,2 @@
+module ThoughtsHelper
+end
3 app/models/day.rb
@@ -0,0 +1,3 @@
+class Day < ActiveRecord::Base
+ has_many :thoughts
+end
6 app/models/thought.rb
@@ -0,0 +1,6 @@
+class Thought < ActiveRecord::Base
+ belongs_to :day
+ belongs_to :user
+
+ validates_presence_of :title, :value
+end
60 app/models/user.rb
@@ -0,0 +1,60 @@
+require 'digest/sha1'
+
+# this model expects a certain database layout and its based on the name/login pattern.
+class User < ActiveRecord::Base
+ has_many :thoughts
+
+ # Please change the salt to something else,
+ # Every application should use a different one
+ @@salt = 'FOOMANCHU'
+ cattr_accessor :salt
+
+ # Authenticate a user.
+ #
+ # Example:
+ # @user = User.authenticate('bob', 'bobpass')
+ #
+ def self.authenticate(login, pass)
+ find_first(["login = ? AND password = ?", login, sha1(pass)])
+ end
+
+
+ protected
+
+ # Apply SHA1 encryption to the supplied password.
+ # We will additionally surround the password with a salt
+ # for additional security.
+ def self.sha1(pass)
+ Digest::SHA1.hexdigest("#{salt}--#{pass}--")
+ end
+
+ before_create :crypt_password
+
+ # Before saving the record to database we will crypt the password
+ # using SHA1.
+ # We never store the actual password in the DB.
+ def crypt_password
+ write_attribute "password", self.class.sha1(password)
+ end
+
+ before_update :crypt_unless_empty
+
+ # If the record is updated we will check if the password is empty.
+ # If its empty we assume that the user didn't want to change his
+ # password and just reset it to the old value.
+ def crypt_unless_empty
+ if password.empty?
+ user = self.class.find(self.id)
+ self.password = user.password
+ else
+ write_attribute "password", self.class.sha1(password)
+ end
+ end
+
+ validates_uniqueness_of :login, :on => :create
+
+ validates_confirmation_of :password
+ validates_length_of :login, :within => 2..40
+ validates_length_of :password, :within => 4..40
+ validates_presence_of :login, :password, :password_confirmation
+end
22 app/views/account/login.rhtml
@@ -0,0 +1,22 @@
+<%= start_form_tag :action=> "login" %>
+
+<div title="Account login" id="loginform" class="form">
+ <h3>Please login</h3>
+
+ <% if @flash['notice'] %>
+ <div id="message"><%= @flash['notice'] %></div>
+ <% end %>
+
+ <label for="user_login">Login:</label><br/>
+ <input type="text" name="user_login" id="user_login" size="30" value=""/><br/>
+
+ <label for="user_password">Password:</label><br/>
+ <input type="password" name="user_password" id="user_password" size="30"/>
+
+ <br/>
+ <input type="submit" name="login" value="Login &#187;" class="primary" />
+
+</div>
+
+<%= end_form_tag %>
+
17 app/views/account/signup.rhtml
@@ -0,0 +1,17 @@
+<%= start_form_tag :action=> "signup" %>
+
+<div title="Account signup" id="signupform" class="form">
+ <h3>Signup</h3>
+ <%= error_messages_for 'user' %><br/>
+
+ <label for="user_login">Desired login:</label><br/>
+ <%= text_field "user", "login", :size => 30 %><br/>
+ <label for="user_password">Choose password:</label><br/>
+ <%= password_field "user", "password", :size => 30 %><br/>
+ <label for="user_password_confirmation">Confirm password:</label><br/>
+ <%= password_field "user", "password_confirmation", :size => 30 %><br/>
+
+<input type="submit" value="Signup &#187;" class="primary" />
+
+<%= end_form_tag %>
+
12 app/views/days/_thoughts.rhtml
@@ -0,0 +1,12 @@
+<h3><%= @day.date.strftime("%a %b %d, %Y") %></h3>
+<div class="thoughts">
+ <% for thought in @day.thoughts.reverse %>
+ <div class="thought" title="<%= thought.title %>">
+ <a href="/day/<%= @day.id %>#thought-<%= thought.id %>" class="permalink" title="Permanent link to this thought"><span>Link</span></a>
+ <a name="thought-<%= thought.id %>"></a>
+ <p>
+ <%= thought.value %>
+ </p>
+ </div>
+<% end %>
+</div>
6 app/views/days/index.rhtml
@@ -0,0 +1,6 @@
+<% for @day in @days %>
+ <%= render(:partial => "thoughts", :object => @day)%>
+<% end %>
+<br/>
+<%= link_to 'Current thoughts', { :action => "index" } if @day_pages.current.previous %>
+<%= link_to 'Older thoughts &raquo;', { :page => @day_pages.current.next } if @day_pages.current.next %>
1 app/views/days/show.rhtml
@@ -0,0 +1 @@
+<%= render(:partial => "thoughts", :object => @day)%>
17 app/views/layouts/application.rhtml
@@ -0,0 +1,17 @@
+<html>
+<head>
+ <title>ThoughtStream</title>
+ <%= stylesheet_link_tag 'thoughts' %>
+ <link rel="alternate" type="application/rss+xml" title="Thought Stream" href="http://feeds.feedburner.com/thoughtstream">
+</head>
+<body>
+<div id="container">
+ <%= link_to(image_tag("name.gif"), :controller => "days", :action => "index" ) %>
+ <div id="sidebar">
+ </div>
+ <div id="content">
+ <%= @content_for_layout %>
+ </div>
+</div>
+</body>
+</html>
6 app/views/thoughts/_form.rhtml
@@ -0,0 +1,6 @@
+<%= error_messages_for 'thought' %>
+
+<b>Title:</b> <%= text_field 'thought', 'title' %><br/>
+<b>Value:</b><br/>
+<%= text_area 'thought', 'value' %>
+
9 app/views/thoughts/edit.rhtml
@@ -0,0 +1,9 @@
+<h1>Editing thought</h1>
+
+<%= start_form_tag :action => 'update', :id => @thought %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag 'Edit' %>
+<%= end_form_tag %>
+
+<%= link_to 'Show', :action => 'show', :id => @thought %> |
+<%= link_to 'List', :action => 'list' %>
22 app/views/thoughts/list.rhtml
@@ -0,0 +1,22 @@
+<%= link_to 'New thought', :action => 'new' %>
+
+<table>
+ <tr>
+ <th>Title</th>
+ <th>Posted</th>
+ </tr>
+
+<% for thought in @thoughts.reverse %>
+ <tr>
+ <td><%= link_to thought.title, :action => 'edit', :id => thought.id %></td>
+ <td><%= thought.created_at %></td>
+ <td><%= link_to 'Show', :controller => 'days', :action => 'show', :id => thought.day.id, :anchor => "thought-#{thought.id}" %></td>
+ <td><%= link_to 'Edit', :action => 'edit', :id => thought %></td>
+ <td><%= link_to 'Destroy', { :action => 'destroy', :id => thought }, :confirm => 'Are you sure?' %></td>
+ </tr>
+<% end %>
+</table>
+
+<br />
+
+
8 app/views/thoughts/new.rhtml
@@ -0,0 +1,8 @@
+<h1>New thought</h1>
+
+<%= start_form_tag :action => 'create' %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag "Create" %>
+<%= end_form_tag %>
+
+<%= link_to 'Back', :action => 'list' %>
23 app/views/thoughts/rss.rxml
@@ -0,0 +1,23 @@
+xml.instruct!
+
+xml.rss "version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/" do
+ xml.channel do
+
+ xml.title "Thought Stream"
+ xml.link url_for(:only_path => false, :controller => 'rss')
+ xml.pubDate CGI.rfc1123_date(@thoughts.first.day.date) if @thoughts.any?
+ xml.description "Thought Stream (aka a tumblelog)"
+
+ @thoughts.each do |thought|
+ xml.item do
+ xml.title thought.title
+ xml.link url_for(:only_path => false, :controller => 'days', :action => 'show', :id => thought.day.id, :anchor => "thought-#{thought.id}")
+ xml.description thought.value
+ xml.pubDate CGI.rfc1123_date(thought.created_at)
+ xml.guid url_for(:only_path => false, :controller => 'days', :action => 'show', :id => "#{thought.day.id}-#{thought.id}")
+ xml.author "#{thought.user.login} <your@email.com>"
+ end
+ end
+
+ end
+end
23 config/database.yml
@@ -0,0 +1,23 @@
+development:
+ adapter: mysql
+ database: thoughts_production
+ host: localhost
+ username:
+ password:
+
+# Warning: The database defined as 'test' will be erased and
+# re-generated from your development database when you run 'rake'.
+# Do not set this db to the same as development or production.
+test:
+ adapter: mysql
+ database: thoughts_test
+ host: localhost
+ username: root
+ password:
+
+production:
+ adapter: mysql
+ database: thoughts_production
+ host: localhost
+ username:
+ password:
87 config/environment.rb
@@ -0,0 +1,87 @@
+# Load the Rails framework and configure your application.
+# You can include your own configuration at the end of this file.
+#
+# Be sure to restart your webserver when you modify this file.
+
+# The path to the root directory of your application.
+RAILS_ROOT = File.join(File.dirname(__FILE__), '..')
+
+# The environment your application is currently running. Don't set
+# this here; put it in your webserver's configuration as the RAILS_ENV
+# environment variable instead.
+#
+# See config/environments/*.rb for environment-specific configuration.
+RAILS_ENV = ENV['RAILS_ENV'] || 'development'
+
+
+# Load the Rails framework. Mock classes for testing come first.
+ADDITIONAL_LOAD_PATHS = ["#{RAILS_ROOT}/test/mocks/#{RAILS_ENV}"]
+
+# Then model subdirectories.
+ADDITIONAL_LOAD_PATHS.concat(Dir["#{RAILS_ROOT}/app/models/[_a-z]*"])
+ADDITIONAL_LOAD_PATHS.concat(Dir["#{RAILS_ROOT}/components/[_a-z]*"])
+
+# Followed by the standard includes.
+ADDITIONAL_LOAD_PATHS.concat %w(
+ app
+ app/models
+ app/controllers
+ app/helpers
+ app/apis
+ components
+ config
+ lib
+ vendor
+ vendor/rails/railties
+ vendor/rails/railties/lib
+ vendor/rails/actionpack/lib
+ vendor/rails/activesupport/lib
+ vendor/rails/activerecord/lib
+ vendor/rails/actionmailer/lib
+ vendor/rails/actionwebservice/lib
+).map { |dir| "#{RAILS_ROOT}/#{dir}" }.select { |dir| File.directory?(dir) }
+
+# Prepend to $LOAD_PATH
+ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) }
+
+# Require Rails libraries.
+require 'rubygems' unless File.directory?("#{RAILS_ROOT}/vendor/rails")
+
+require 'active_support'
+require 'active_record'
+require 'action_controller'
+require 'action_mailer'
+require 'action_web_service'
+
+# Environment-specific configuration.
+require_dependency "environments/#{RAILS_ENV}"
+ActiveRecord::Base.configurations = File.open("#{RAILS_ROOT}/config/database.yml") { |f| YAML::load(f) }
+ActiveRecord::Base.establish_connection
+
+
+# Configure defaults if the included environment did not.
+begin
+ RAILS_DEFAULT_LOGGER = Logger.new("#{RAILS_ROOT}/log/#{RAILS_ENV}.log")
+ RAILS_DEFAULT_LOGGER.level = (RAILS_ENV == 'production' ? Logger::INFO : Logger::DEBUG)
+rescue StandardError
+ RAILS_DEFAULT_LOGGER = Logger.new(STDERR)
+ RAILS_DEFAULT_LOGGER.level = Logger::WARN
+ RAILS_DEFAULT_LOGGER.warn(
+ "Rails Error: Unable to access log file. Please ensure that log/#{RAILS_ENV}.log exists and is chmod 0666. " +
+ "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed."
+ )
+end
+
+[ActiveRecord, ActionController, ActionMailer].each { |mod| mod::Base.logger ||= RAILS_DEFAULT_LOGGER }
+[ActionController, ActionMailer].each { |mod| mod::Base.template_root ||= "#{RAILS_ROOT}/app/views/" }
+
+# Set up routes.
+ActionController::Routing::Routes.reload
+
+Controllers = Dependencies::LoadingModule.root(
+ File.join(RAILS_ROOT, 'app', 'controllers'),
+ File.join(RAILS_ROOT, 'components')
+)
+
+# Include your app's configuration here:
+
14 config/environments/development.rb
@@ -0,0 +1,14 @@
+# In the development environment your application's code is reloaded on
+# every request. This slows down response time but is perfect for development
+# since you don't have to restart the webserver when you make code changes.
+
+# Log error messages when you accidentally call methods on nil.
+require 'active_support/whiny_nil'
+
+# Reload code; show full error reports; disable caching.
+Dependencies.mechanism = :load
+ActionController::Base.consider_all_requests_local = true
+ActionController::Base.perform_caching = false
+
+# The breakpoint server port that script/breakpointer connects to.
+BREAKPOINT_SERVER_PORT = 42531
8 config/environments/production.rb
@@ -0,0 +1,8 @@
+# The production environment is meant for finished, "live" apps.
+# Code is not reloaded between requests, full error reports are disabled,
+# and caching is turned on.
+
+# Don't reload code; don't show full error reports; enable caching.
+Dependencies.mechanism = :require
+ActionController::Base.consider_all_requests_local = false
+ActionController::Base.perform_caching = true
17 config/environments/test.rb
@@ -0,0 +1,17 @@
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+
+# Log error messages when you accidentally call methods on nil.
+require 'active_support/whiny_nil'
+
+# Don't reload code; show full error reports; disable caching.
+Dependencies.mechanism = :require
+ActionController::Base.consider_all_requests_local = true
+ActionController::Base.perform_caching = false
+
+# Tell ActionMailer not to deliver emails to the real world.
+# The :test delivery method accumulates sent emails in the
+# ActionMailer::Base.deliveries array.
+ActionMailer::Base.delivery_method = :test
31 config/routes.rb
@@ -0,0 +1,31 @@
+ActionController::Routing::Routes.draw do |map|
+ # Add your own custom routes here.
+ # The priority is based upon order of creation: first created -> highest priority.
+
+ # Here's a sample route:
+ # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
+ # Keep in mind you can assign values other than :controller and :action
+
+ map.connect '', :controller => "days", :action => "index"
+
+ map.connect 'thoughts', :controller => "days", :action => "index"
+ map.connect 'day/:id', :controller => "days", :action => "show"
+ map.connect 'list', :controller => "thoughts", :action => "list"
+
+ # nicer rss feed link
+ map.connect 'rss', :controller => "thoughts", :action => "rss"
+
+ # account stuff
+ map.connect 'signup', :controller => "account", :action => "signup"
+ map.connect 'login', :controller => "account", :action => "login"
+ map.connect 'logout', :controller => "account", :action => "logout"
+
+ map.connect 'Euro2005', :controller => "euro", :action => "index"
+
+# Allow downloading Web Service WSDL as a file with an extension
+ # instead of a file named 'wsdl'
+ #map.connect ':controller/service.wsdl', :action => 'wsdl'
+
+ # Install the default route as the lowest priority.
+ map.connect ':controller/:action/:id'
+end
22 db/thoughts.sql
@@ -0,0 +1,22 @@
+CREATE TABLE `days` (
+ `id` int(11) NOT NULL auto_increment,
+ `date` timestamp NOT NULL default '0000-00-00 00:00:00',
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `thoughts` (
+ `id` int(11) NOT NULL auto_increment,
+ `title` varchar(255) default NULL,
+ `value` text,
+ `created_at` timestamp NOT NULL default '0000-00-00 00:00:00',
+ `day_id` int(11) default NULL,
+ `user_id` int(11) default NULL,
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `users` (
+ `id` int(11) NOT NULL auto_increment,
+ `login` varchar(80) default NULL,
+ `password` varchar(40) default NULL,
+ PRIMARY KEY (`id`)
+);
0 doc/README_FOR_APP
No changes.
87 lib/login_system.rb
@@ -0,0 +1,87 @@
+require_dependency "user"
+
+module LoginSystem
+
+ 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 @session[: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
+ return false
+ 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=>"/account", :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_back_or_default(default)
+ if @session[:return_to].nil?
+ redirect_to default
+ else
+ redirect_to_url @session[:return_to]
+ @session[:return_to] = nil
+ end
+ end
+
+end
32 public/.htaccess
@@ -0,0 +1,32 @@
+# General Apache options
+AddHandler fastcgi-script .fcgi
+AddHandler cgi-script .cgi
+Options +FollowSymLinks +ExecCGI
+
+# If you don't want Rails to look in certain directories,
+# use the following rewrite rules so that Apache won't rewrite certain requests
+#
+# Example:
+# RewriteCond %{REQUEST_URI} ^/notrails.*
+# RewriteRule .* - [L]
+
+# Redirect all requests not available on the filesystem to Rails
+# By default the cgi dispatcher is used which is very slow
+#
+# For better performance replace the dispatcher with the fastcgi one
+#
+# Example:
+# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
+RewriteEngine On
+RewriteRule ^$ index.html [QSA]
+RewriteRule ^([^.]+)$ $1.html [QSA]
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
+
+# In case Rails experiences terminal errors
+# Instead of displaying this message you can supply a file here which will be rendered instead
+#
+# Example:
+# ErrorDocument 500 /500.html
+
+ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"
8 public/404.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<body>
+ <h1>File not found</h1>
+ <p>Change this error message for pages not found in public/404.html</p>
+</body>
+</html>
8 public/500.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<body>
+ <h1>Application error (Apache)</h1>
+ <p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p>
+</body>
+</html>
24 public/dispatch.fcgi
@@ -0,0 +1,24 @@
+#!/usr/bin/ruby1.8
+#
+# You may specify the path to the FastCGI crash log (a log of unhandled
+# exceptions which forced the FastCGI instance to exit, great for debugging)
+# and the number of requests to process before running garbage collection.
+#
+# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
+# and the GC period is nil (turned off). A reasonable number of requests
+# could range from 10-100 depending on the memory footprint of your app.
+#
+# Example:
+# # Default log path, normal GC behavior.
+# RailsFCGIHandler.process!
+#
+# # Default log path, 50 requests between GC.
+# RailsFCGIHandler.process! nil, 50
+#
+# # Custom log path, normal GC behavior.
+# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
+#
+require File.dirname(__FILE__) + "/../config/environment"
+require 'fcgi_handler'
+
+RailsFCGIHandler.process!
10 public/dispatch.rb
@@ -0,0 +1,10 @@
+#!/usr/bin/ruby1.8
+
+require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
+require "dispatcher"
+
+ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
+Dispatcher.dispatch
0 public/favicon.ico
No changes.
BIN public/images/blockquote.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN public/images/divider.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN public/images/name.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN public/images/permalink.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN public/images/uploads/dynamite.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN public/images/uploads/eiffel.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN public/images/uploads/einstein_ar.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN public/images/uploads/scooter.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
446 public/javascripts/controls.js
@@ -0,0 +1,446 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+//
+// 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.
+
+Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
+ var children = $(element).childNodes;
+ var text = "";
+ var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");
+
+ for (var i = 0; i < children.length; i++) {
+ if(children[i].nodeType==3) {
+ text+=children[i].nodeValue;
+ } else {
+ if((!children[i].className.match(classtest)) && children[i].hasChildNodes())
+ text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);
+ }
+ }
+
+ return text;
+}
+
+// Autocompleter.Base handles all the autocompletion functionality
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least,
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method
+// should get the text for which to provide autocompletion by
+// invoking this.getEntry(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: new Array (',', '\n') } which
+// enables autocompletion on multiple tokens. This is most
+// useful when one of the tokens is \n (a newline), as it
+// allows smart autocompletion after linebreaks.
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+ base_initialize: function(element, update, options) {
+ this.element = $(element);
+ this.update = $(update);
+ this.has_focus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entry_count = 0;
+
+ if (this.setOptions)
+ this.setOptions(options);
+ else
+ this.options = {}
+
+ this.options.tokens = this.options.tokens || new Array();
+ this.options.frequency = this.options.frequency || 0.4;
+ this.options.min_chars = this.options.min_chars || 1;
+ this.options.onShow = this.options.onShow ||
+ function(element, update){
+ if(!update.style.position || update.style.position=='absolute') {
+ update.style.position = 'absolute';
+ var offsets = Position.cumulativeOffset(element);
+ update.style.left = offsets[0] + 'px';
+ update.style.top = (offsets[1] + element.offsetHeight) + 'px';
+ update.style.width = element.offsetWidth + 'px';
+ }
+ new Effect.Appear(update,{duration:0.15});
+ };
+ this.options.onHide = this.options.onHide ||
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+ if(this.options.indicator)
+ this.indicator = $(this.options.indicator);
+
+ if (typeof(this.options.tokens) == 'string')
+ this.options.tokens = new Array(this.options.tokens);
+
+ this.observer = null;
+
+ Element.hide(this.update);
+
+ Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(this.update.style.display=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && this.update.style.position=='absolute') {
+ new Insertion.After(this.update,
+ '<iframe id="' + this.update.id + '_iefix" '+
+ 'style="display:none;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) {
+ Position.clone(this.update, this.iefix);
+ this.iefix.style.zIndex = 1;
+ this.update.style.zIndex = 2;
+ Element.show(this.iefix);
+ }
+ },
+
+ hide: function() {
+ if(this.update.style.display=='') this.options.onHide(this.element, this.update);
+ if(this.iefix) Element.hide(this.iefix);
+ },
+
+ startIndicator: function() {
+ if(this.indicator) Element.show(this.indicator);
+ },
+
+ stopIndicator: function() {
+ if(this.indicator) Element.hide(this.indicator);
+ },
+
+ onKeyPress: function(event) {
+ if(this.active)
+ switch(event.keyCode) {
+ case Event.KEY_TAB:
+ case Event.KEY_RETURN:
+ this.select_entry();
+ Event.stop(event);
+ case Event.KEY_ESC:
+ this.hide();
+ this.active = false;
+ return;
+ case Event.KEY_LEFT:
+ case Event.KEY_RIGHT:
+ return;
+ case Event.KEY_UP:
+ this.mark_previous();
+ this.render();
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.mark_next();
+ this.render();
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
+ return;
+
+ this.changed = true;
+ this.has_focus = true;
+
+ if(this.observer) clearTimeout(this.observer);
+ this.observer =
+ setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+ },
+
+ onHover: function(event) {
+ var element = Event.findElement(event, 'LI');
+ if(this.index != element.autocompleteIndex)
+ {
+ this.index = element.autocompleteIndex;
+ this.render();
+ }
+ Event.stop(event);
+ },
+
+ onClick: function(event) {
+ var element = Event.findElement(event, 'LI');
+ this.index = element.autocompleteIndex;
+ this.select_entry();
+ Event.stop(event);
+ },
+
+ onBlur: function(event) {
+ // needed to make click events working
+ setTimeout(this.hide.bind(this), 250);
+ this.has_focus = false;
+ this.active = false;
+ },
+
+ render: function() {
+ if(this.entry_count > 0) {
+ for (var i = 0; i < this.entry_count; i++)
+ this.index==i ?
+ Element.addClassName(this.get_entry(i),"selected") :
+ Element.removeClassName(this.get_entry(i),"selected");
+
+ if(this.has_focus) {
+ if(this.get_current_entry().scrollIntoView)
+ this.get_current_entry().scrollIntoView(false);
+
+ this.show();
+ this.active = true;
+ }
+ } else this.hide();
+ },
+
+ mark_previous: function() {
+ if(this.index > 0) this.index--
+ else this.index = this.entry_count-1;
+ },
+
+ mark_next: function() {
+ if(this.index < this.entry_count-1) this.index++
+ else this.index = 0;
+ },
+
+ get_entry: function(index) {
+ return this.update.firstChild.childNodes[index];
+ },
+
+ get_current_entry: function() {
+ return this.get_entry(this.index);
+ },
+
+ select_entry: function() {
+ this.active = false;
+ value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML();
+ this.updateElement(value);
+ this.element.focus();
+ },
+
+ updateElement: function(value) {
+ var last_token_pos = this.findLastToken();
+ if (last_token_pos != -1) {
+ var new_value = this.element.value.substr(0, last_token_pos + 1);
+ var whitespace = this.element.value.substr(last_token_pos + 1).match(/^\s+/);
+ if (whitespace)
+ new_value += whitespace[0];
+ this.element.value = new_value + value;
+ } else {
+ this.element.value = value;
+ }
+ },
+
+ updateChoices: function(choices) {
+ if(!this.changed && this.has_focus) {
+ this.update.innerHTML = choices;
+ Element.cleanWhitespace(this.update);
+ Element.cleanWhitespace(this.update.firstChild);
+
+ if(this.update.firstChild && this.update.firstChild.childNodes) {
+ this.entry_count =
+ this.update.firstChild.childNodes.length;
+ for (var i = 0; i < this.entry_count; i++) {
+ entry = this.get_entry(i);
+ entry.autocompleteIndex = i;
+ this.addObservers(entry);
+ }
+ } else {
+ this.entry_count = 0;
+ }
+
+ this.stopIndicator();
+
+ this.index = 0;
+ this.render();
+ }
+ },
+
+ addObservers: function(element) {
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+ },
+
+ onObserverEvent: function() {
+ this.changed = false;
+ if(this.getEntry().length>=this.options.min_chars) {
+ this.startIndicator();
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ getEntry: function() {
+ var token_pos = this.findLastToken();
+ if (token_pos != -1)
+ var ret = this.element.value.substr(token_pos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+ else
+ var ret = this.element.value;
+
+ return /\n/.test(ret) ? '' : ret;
+ },
+
+ findLastToken: function() {
+ var last_token_pos = -1;
+
+ for (var i=0; i<this.options.tokens.length; i++) {
+ var this_token_pos = this.element.value.lastIndexOf(this.options.tokens[i]);
+ if (this_token_pos > last_token_pos)
+ last_token_pos = this_token_pos;
+ }
+ return last_token_pos;
+ }
+}
+
+Ajax.Autocompleter = Class.create();
+Ajax.Autocompleter.prototype = Object.extend(new Autocompleter.Base(),
+Object.extend(new Ajax.Base(), {
+ initialize: function(element, update, url, options) {
+ this.base_initialize(element, update, options);
+ this.options.asynchronous = true;
+ this.options.onComplete = this.onComplete.bind(this)
+ this.options.method = 'post';
+ this.options.defaultParams = this.options.parameters || null;
+ this.url = url;
+ },
+
+ getUpdatedChoices: function() {
+ entry = encodeURIComponent(this.element.name) + '=' +
+ encodeURIComponent(this.getEntry());
+
+ this.options.parameters = this.options.callback ?
+ this.options.callback(this.element, entry) : entry;
+
+ if(this.options.defaultParams)
+ this.options.parameters += '&' + this.options.defaultParams;
+
+ new Ajax.Request(this.url, this.options);
+ },
+
+ onComplete: function(request) {
+ this.updateChoices(request.responseText);
+ }
+
+}));
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partial_search - If false, the autocompleter will match entered
+// text only at the beginning of strings in the
+// autocomplete array. Defaults to true, which will
+// match text at the beginning of any *word* in the
+// strings in the autocomplete array. If you want to
+// search anywhere in the string, additionally set
+// the option full_search to true (default: off).
+//
+// - full_search - Search anywhere in autocomplete array strings.
+//
+// - partial_chars - How many characters to enter before triggering
+// a partial match (unlike min_chars, which defines
+// how many characters are required to do any match
+// at all). Defaults to 2.
+//
+// - ignore_case - Whether to ignore case when autocompleting.
+// Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector'
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create();
+Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
+ initialize: function(element, update, array, options) {
+ this.base_initialize(element, update, options);
+ this.options.array = array;
+ },
+
+ getUpdatedChoices: function() {
+ this.updateChoices(this.options.selector(this));
+ },
+
+ setOptions: function(options) {
+ this.options = Object.extend({
+ choices: 10,
+ partial_search: true,
+ partial_chars: 2,
+ ignore_case: true,
+ full_search: false,
+ selector: function(instance) {
+ var ret = new Array(); // Beginning matches
+ var partial = new Array(); // Inside matches
+ var entry = instance.getEntry();
+ var count = 0;
+
+ for (var i = 0; i < instance.options.array.length &&
+ ret.length < instance.options.choices ; i++) {
+ var elem = instance.options.array[i];
+ var found_pos = instance.options.ignore_case ?
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
+ elem.indexOf(entry);
+
+ while (found_pos != -1) {
+ if (found_pos == 0 && elem.length != entry.length) {
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
+ elem.substr(entry.length) + "</li>");
+ break;
+ } else if (entry.length >= instance.options.partial_chars &&
+ instance.options.partial_search && found_pos != -1) {
+ if (instance.options.full_search || /\s/.test(elem.substr(found_pos-1,1))) {
+ partial.push("<li>" + elem.substr(0, found_pos) + "<strong>" +
+ elem.substr(found_pos, entry.length) + "</strong>" + elem.substr(
+ found_pos + entry.length) + "</li>");
+ break;
+ }
+ }
+
+ found_pos = instance.options.ignore_case ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), found_pos + 1) :
+ elem.indexOf(entry, found_pos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+ return "<ul>" + ret.join('') + "</ul>";
+ }
+ }, options || {});
+ }
+});
537 public/javascripts/dragdrop.js
@@ -0,0 +1,537 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// Element.Class part Copyright (c) 2005 by Rick Olson
+//
+// 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.
+
+Element.Class = {
+ // Element.toggleClass(element, className) toggles the class being on/off
+ // Element.toggleClass(element, className1, className2) toggles between both classes,
+ // defaulting to className1 if neither exist
+ toggle: function(element, className) {
+ if(Element.Class.has(element, className)) {
+ Element.Class.remove(element, className);
+ if(arguments.length == 3) Element.Class.add(element, arguments[2]);
+ } else {
+ Element.Class.add(element, className);
+ if(arguments.length == 3) Element.Class.remove(element, arguments[2]);
+ }
+ },
+
+ // gets space-delimited classnames of an element as an array
+ get: function(element) {
+ element = $(element);
+ return element.className.split(' ');
+ },
+
+ // functions adapted from original functions by Gavin Kistner
+ remove: function(element) {
+ element = $(element);
+ var regEx;
+ for(var i = 1; i < arguments.length; i++) {
+ regEx = new RegExp("^" + arguments[i] + "\\b\\s*|\\s*\\b" + arguments[i] + "\\b", 'g');
+ element.className = element.className.replace(regEx, '')
+ }
+ },
+
+ add: function(element) {
+ element = $(element);
+ for(var i = 1; i < arguments.length; i++) {
+ Element.Class.remove(element, arguments[i]);
+ element.className += (element.className.length > 0 ? ' ' : '') + arguments[i];
+ }
+ },
+
+ // returns true if all given classes exist in said element
+ has: function(element) {
+ element = $(element);
+ if(!element || !element.className) return false;
+ var regEx;
+ for(var i = 1; i < arguments.length; i++) {
+ regEx = new RegExp("\\b" + arguments[i] + "\\b");
+ if(!regEx.test(element.className)) return false;
+ }
+ return true;
+ },
+
+ // expects arrays of strings and/or strings as optional paramters
+ // Element.Class.has_any(element, ['classA','classB','classC'], 'classD')
+ has_any: function(element) {
+ element = $(element);
+ if(!element || !element.className) return false;
+ var regEx;
+ for(var i = 1; i < arguments.length; i++) {
+ if((typeof arguments[i] == 'object') &&
+ (arguments[i].constructor == Array)) {
+ for(var j = 0; j < arguments[i].length; j++) {
+ regEx = new RegExp("\\b" + arguments[i][j] + "\\b");
+ if(regEx.test(element.className)) return true;
+ }
+ } else {
+ regEx = new RegExp("\\b" + arguments[i] + "\\b");
+ if(regEx.test(element.className)) return true;
+ }
+ }
+ return false;
+ },
+
+ childrenWith: function(element, className) {
+ var children = $(element).getElementsByTagName('*');
+ var elements = new Array();
+
+ for (var i = 0; i < children.length; i++) {
+ if (Element.Class.has(children[i], className)) {
+ elements.push(children[i]);
+ break;
+ }
+ }
+
+ return elements;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Droppables = {
+ drops: false,
+
+ remove: function(element) {
+ for(var i = 0; i < this.drops.length; i++)
+ if(this.drops[i].element == element)
+ this.drops.splice(i,1);
+ },
+
+ add: function(element) {
+ var element = $(element);
+ var options = Object.extend({
+ greedy: true,
+ hoverclass: null
+ }, arguments[1] || {});
+
+ // cache containers
+ if(options.containment) {
+ options._containers = new Array();
+ var containment = options.containment;
+ if((typeof containment == 'object') &&
+ (containment.constructor == Array)) {
+ for(var i=0; i<containment.length; i++)
+ options._containers.push($(containment[i]));
+ } else {
+ options._containers.push($(containment));
+ }
+ options._containers_length =
+ options._containers.length-1;
+ }
+
+ Element.makePositioned(element); // fix IE
+
+ options.element = element;
+
+ // activate the droppable
+ if(!this.drops) this.drops = [];
+ this.drops.push(options);
+ },
+
+ is_contained: function(element, drop) {
+ var containers = drop._containers;
+ var parentNode = element.parentNode;
+ var i = drop._containers_length;
+ do { if(parentNode==containers[i]) return true; } while (i--);
+ return false;
+ },
+
+ is_affected: function(pX, pY, element, drop) {
+ return (
+ (drop.element!=element) &&
+ ((!drop._containers) ||
+ this.is_contained(element, drop)) &&
+ ((!drop.accept) ||
+ (Element.Class.has_any(element, drop.accept))) &&
+ Position.within(drop.element, pX, pY) );
+ },
+
+ deactivate: function(drop) {
+ Element.Class.remove(drop.element, drop.hoverclass);
+ this.last_active = null;
+ },
+
+ activate: function(drop) {
+ if(this.last_active) this.deactivate(this.last_active);
+ if(drop.hoverclass) {
+ Element.Class.add(drop.element, drop.hoverclass);
+ this.last_active = drop;
+ }
+ },
+
+ show: function(event, element) {
+ if(!this.drops) return;
+ var pX = Event.pointerX(event);
+ var pY = Event.pointerY(event);
+ Position.prepare();
+
+ var i = this.drops.length-1; do {
+ var drop = this.drops[i];
+ if(this.is_affected(pX, pY, element, drop)) {
+ if(drop.onHover)
+ drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+ if(drop.greedy) {
+ this.activate(drop);
+ return;
+ }
+ }
+ } while (i--);
+ },
+
+ fire: function(event, element) {
+ if(!this.last_active) return;
+ Position.prepare();
+
+ if (this.is_affected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
+ if (this.last_active.onDrop)
+ this.last_active.onDrop(element, this.last_active);
+
+ },
+
+ reset: function() {
+ if(this.last_active)
+ this.deactivate(this.last_active);
+ }
+}
+
+Draggables = {
+ observers: new Array(),
+ addObserver: function(observer) {
+ this.observers.push(observer);
+ },
+ removeObserver: function(element) { // element instead of obsever fixes mem leaks
+ for(var i = 0; i < this.observers.length; i++)
+ if(this.observers[i].element && (this.observers[i].element == element))
+ this.observers.splice(i,1);
+ },
+ notify: function(eventName, draggable) { // 'onStart', 'onEnd'
+ for(var i = 0; i < this.observers.length; i++)
+ this.observers[i][eventName](draggable);
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Draggable = Class.create();
+Draggable.prototype = {
+ initialize: function(element) {
+ var options = Object.extend({
+ handle: false,
+ starteffect: function(element) {
+ new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
+ },
+ reverteffect: function(element, top_offset, left_offset) {
+ new Effect.MoveBy(element, -top_offset, -left_offset, {duration:0.4});
+ },
+ endeffect: function(element) {
+ new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
+ },
+ zindex: 1000,
+ revert: false
+ }, arguments[1] || {});
+
+ this.element = $(element);
+ this.handle = options.handle ? $(options.handle) : this.element;
+
+ Element.makePositioned(this.element); // fix IE
+
+ this.offsetX = 0;
+ this.offsetY = 0;
+ this.originalLeft = this.currentLeft();
+ this.originalTop = this.currentTop();
+ this.originalX = this.element.offsetLeft;
+ this.originalY = this.element.offsetTop;
+ this.originalZ = parseInt(this.element.style.zIndex || "0");
+
+ this.options = options;
+
+ this.active = false;
+ this.dragging = false;
+
+ this.eventMouseDown = this.startDrag.bindAsEventListener(this);
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
+ this.eventMouseMove = this.update.bindAsEventListener(this);
+ this.eventKeypress = this.keyPress.bindAsEventListener(this);
+
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
+ Event.observe(document, "mouseup", this.eventMouseUp);
+ Event.observe(document, "mousemove", this.eventMouseMove);
+ Event.observe(document, "keypress", this.eventKeypress);
+ },
+ destroy: function() {
+ Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
+ Event.stopObserving(document, "keypress", this.eventKeypress);
+ },
+ currentLeft: function() {
+ return parseInt(this.element.style.left || '0');
+ },
+ currentTop: function() {
+ return parseInt(this.element.style.top || '0')
+ },
+ startDrag: function(event) {
+ if(Event.isLeftClick(event)) {
+ this.active = true;
+
+ var style = this.element.style;
+ this.originalY = this.element.offsetTop - this.currentTop() - this.originalTop;
+ this.originalX = this.element.offsetLeft - this.currentLeft() - this.originalLeft;
+ this.offsetY = event.clientY - this.originalY - this.originalTop;
+ this.offsetX = event.clientX - this.originalX - this.originalLeft;
+
+ Event.stop(event);
+ }
+ },
+ finishDrag: function(event, success) {
+ this.active = false;
+ this.dragging = false;
+
+ if(success) Droppables.fire(event, this.element);
+ Draggables.notify('onEnd', this);
+
+ var revert = this.options.revert;
+ if(revert && typeof revert == 'function') revert = revert(this.element);
+
+ if(revert && this.options.reverteffect) {
+ this.options.reverteffect(this.element,
+ this.currentTop()-this.originalTop,
+ this.currentLeft()-this.originalLeft);
+ } else {
+ this.originalLeft = this.currentLeft();
+ this.originalTop = this.currentTop();
+ }
+
+ this.element.style.zIndex = this.originalZ;
+
+ if(this.options.endeffect)
+ this.options.endeffect(this.element);
+
+ Droppables.reset();
+ },
+ keyPress: function(event) {
+ if(this.active) {
+ if(event.keyCode==Event.KEY_ESC) {
+ this.finishDrag(event, false);
+ Event.stop(event);
+ }
+ }
+ },
+ endDrag: function(event) {
+ if(this.active && this.dragging) {
+ this.finishDrag(event, true);
+ Event.stop(event);
+ }
+ this.active = false;
+ this.dragging = false;
+ },
+ draw: function(event) {
+ var style = this.element.style;
+ this.originalX = this.element.offsetLeft - this.currentLeft() - this.originalLeft;
+ this.originalY = this.element.offsetTop - this.currentTop() - this.originalTop;
+ if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+ style.left = ((event.clientX - this.originalX) - this.offsetX) + "px";
+ if((!this.options.constraint) || (this.options.constraint=='vertical'))
+ style.top = ((event.clientY - this.originalY) - this.offsetY) + "px";
+ if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+ },
+ update: function(event) {
+ if(this.active) {
+ if(!this.dragging) {
+ var style = this.element.style;
+ this.dragging = true;
+ if(style.position=="") style.position = "relative";
+ style.zIndex = this.options.zindex;
+ Draggables.notify('onStart', this);
+ if(this.options.starteffect) this.options.starteffect(this.element);
+ }
+
+ Droppables.show(event, this.element);
+ this.draw(event);
+ if(this.options.change) this.options.change(this);
+
+ // fix AppleWebKit rendering
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+
+ Event.stop(event);
+ }
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+SortableObserver = Class.create();
+SortableObserver.prototype = {
+ initialize: function(element, observer) {
+ this.element = $(element);
+ this.observer = observer;
+ this.lastValue = Sortable.serialize(this.element);
+ },
+ onStart: function() {
+ this.lastValue = Sortable.serialize(this.element);
+ },
+ onEnd: function() {
+ if(this.lastValue != Sortable.serialize(this.element))
+ this.observer(this.element)
+ }
+}
+
+Sortable = {
+ sortables: new Array(),
+ options: function(element){
+ var element = $(element);
+ for(var i=0;i<this.sortables.length;i++)
+ if(this.sortables[i].element == element)
+ return this.sortables[i];
+ return null;
+ },
+ destroy: function(element){
+ var element = $(element);
+ for(var i=0;i<this.sortables.length;i++) {
+ if(this.sortables[i].element == element) {
+ var s = this.sortables[i];
+ Draggables.removeObserver(s.element);
+ for(var j=0;j<s.droppables.length;j++)
+ Droppables.remove(s.droppables[j]);
+ for(var j=0;j<s.draggables.length;j++)
+ s.draggables[j].destroy();
+ this.sortables.splice(i,1);
+ }
+ }
+ },
+ create: function(element) {
+ var element = $(element);
+ var options = Object.extend({
+ element: element,
+ tag: 'li', // assumes li children, override with tag: 'tagname'
+ overlap: 'vertical', // one of 'vertical', 'horizontal'
+ constraint: 'vertical', // one of 'vertical', 'horizontal', false
+ containment: element, // also takes array of elements (or id's); or false
+ handle: false, // or a CSS class
+ only: false,
+ hoverclass: null,
+ onChange: function() {},
+ onUpdate: function() {}
+ }, arguments[1] || {});
+
+ // clear any old sortable with same element
+ this.destroy(element);
+
+ // build options for the draggables
+ var options_for_draggable = {
+ revert: true,
+ constraint: options.constraint,
+ handle: handle };
+ if(options.starteffect)
+ options_for_draggable.starteffect = options.starteffect;
+ if(options.reverteffect)
+ options_for_draggable.reverteffect = options.reverteffect;
+ if(options.endeffect)
+ options_for_draggable.endeffect = options.endeffect;
+ if(options.zindex)
+ options_for_draggable.zindex = options.zindex;
+
+ // build options for the droppables
+ var options_for_droppable = {
+ overlap: options.overlap,
+ containment: options.containment,
+ hoverclass: options.hoverclass,
+ onHover: function(element, dropon, overlap) {
+ if(overlap>0.5) {
+ if(dropon.previousSibling != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, dropon);
+ if(dropon.parentNode!=oldParentNode && oldParentNode.sortable)
+ oldParentNode.sortable.onChange(element);
+ if(dropon.parentNode.sortable)
+ dropon.parentNode.sortable.onChange(element);
+ }
+ } else {
+ var nextElement = dropon.nextSibling || null;
+ if(nextElement != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, nextElement);
+ if(dropon.parentNode!=oldParentNode && oldParentNode.sortable)
+ oldParentNode.sortable.onChange(element);
+ if(dropon.parentNode.sortable)
+ dropon.parentNode.sortable.onChange(element);
+ }
+ }
+ }
+ }
+
+ // fix for gecko engine
+ Element.cleanWhitespace(element);
+
+ options.draggables = [];
+ options.droppables = [];
+
+ // make it so
+ var elements = element.childNodes;
+ for (var i = 0; i < elements.length; i++)
+ if(elements[i].tagName && elements[i].tagName==options.tag.toUpperCase() &&
+ (!options.only || (Element.Class.has(elements[i], options.only)))) {
+
+ // handles are per-draggable
+ var handle = options.handle ?
+ Element.Class.childrenWith(elements[i], options.handle)[0] : elements[i];
+
+ options.draggables.push(new Draggable(elements[i], Object.extend(options_for_draggable, { handle: handle })));
+
+ Droppables.add(elements[i], options_for_droppable);
+ options.droppables.push(elements[i]);
+
+ }
+
+ // keep reference
+ this.sortables.push(options);
+
+ // for onupdate
+ Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+ },
+ serialize: function(element) {
+ var element = $(element);
+ var sortableOptions = this.options(element);
+ var options = Object.extend({
+ tag: sortableOptions.tag,
+ only: sortableOptions.only,
+ name: element.id
+ }, arguments[1] || {});
+
+ var items = $(element).childNodes;
+ var queryComponents = new Array();
+
+ for(var i=0; i<items.length; i++)
+ if(items[i].tagName && items[i].tagName==options.tag.toUpperCase() &&
+ (!options.only || (Element.Class.has(items[i], options.only))))
+ queryComponents.push(
+ encodeURIComponent(options.name) + "[]=" +
+ encodeURIComponent(items[i].id.split("_")[1]));
+
+ return queryComponents.join("&");
+ }
+}
612 public/javascripts/effects.js
@@ -0,0 +1,612 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// Parts (c) 2005 Justin Palmer (http://encytemedia.com/)
+// Parts (c) 2005 Mark Pilgrim (http://diveintomark.org/)
+//
+// 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