Submitting new (partial), rails based stats website.

This commit is contained in:
Mykola Konyk 2008-07-08 23:16:07 +00:00
parent d7b2b2006c
commit e75686c82f
133 changed files with 12416 additions and 0 deletions

203
utils/stats/website/README Normal file
View file

@ -0,0 +1,203 @@
== Welcome to Rails
Rails is a web-application and persistence framework that includes everything
needed to create database-backed web-applications according to the
Model-View-Control pattern of separation. This pattern splits the view (also
called the presentation) into "dumb" templates that are primarily responsible
for inserting pre-built data in between HTML tags. The model contains the
"smart" domain objects (such as Account, Product, Person, Post) that holds all
the business logic and knows how to persist themselves to a database. The
controller handles the incoming requests (such as Save New Account, Update
Product, Show Post) by manipulating the model and directing data to the view.
In Rails, the model is handled by what's called an object-relational mapping
layer entitled Active Record. This layer allows you to present the data from
database rows as objects and embellish these data objects with business logic
methods. You can read more about Active Record in
link:files/vendor/rails/activerecord/README.html.
The controller and view are handled by the Action Pack, which handles both
layers by its two parts: Action View and Action Controller. These two layers
are bundled in a single package due to their heavy interdependence. This is
unlike the relationship between the Active Record and Action Pack that is much
more separate. Each of these packages can be used independently outside of
Rails. You can read more about Action Pack in
link:files/vendor/rails/actionpack/README.html.
== Getting Started
1. At the command prompt, start a new Rails application using the <tt>rails</tt> command
and your application name. Ex: rails myapp
(If you've downloaded Rails in a complete tgz or zip, this step is already done)
2. Change directory into myapp and start the web server: <tt>script/server</tt> (run with --help for options)
3. Go to http://localhost:3000/ and get "Welcome aboard: Youre riding the Rails!"
4. Follow the guidelines to start developing your application
== Web Servers
By default, Rails will try to use Mongrel and lighttpd if they are installed, otherwise
Rails will use WEBrick, the webserver that ships with Ruby. When you run script/server,
Rails will check if Mongrel exists, then lighttpd and finally fall back to WEBrick. This ensures
that you can always get up and running quickly.
Mongrel is a Ruby-based webserver with a C component (which requires compilation) that is
suitable for development and deployment of Rails applications. If you have Ruby Gems installed,
getting up and running with mongrel is as easy as: <tt>gem install mongrel</tt>.
More info at: http://mongrel.rubyforge.org
If Mongrel is not installed, Rails will look for lighttpd. It's considerably faster than
Mongrel and WEBrick and also suited for production use, but requires additional
installation and currently only works well on OS X/Unix (Windows users are encouraged
to start with Mongrel). We recommend version 1.4.11 and higher. You can download it from
http://www.lighttpd.net.
And finally, if neither Mongrel or lighttpd are installed, Rails will use the built-in Ruby
web server, WEBrick. WEBrick is a small Ruby web server suitable for development, but not
for production.
But of course its also possible to run Rails on any platform that supports FCGI.
Apache, LiteSpeed, IIS are just a few. For more information on FCGI,
please visit: http://wiki.rubyonrails.com/rails/pages/FastCGI
== Debugging Rails
Sometimes your application goes wrong. Fortunately there are a lot of tools that
will help you debug it and get it back on the rails.
First area to check is the application log files. Have "tail -f" commands running
on the server.log and development.log. Rails will automatically display debugging
and runtime information to these files. Debugging info will also be shown in the
browser on requests from 127.0.0.1.
You can also log your own messages directly into the log file from your code using
the Ruby logger class from inside your controllers. Example:
class WeblogController < ActionController::Base
def destroy
@weblog = Weblog.find(params[:id])
@weblog.destroy
logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!")
end
end
The result will be a message in your log file along the lines of:
Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1
More information on how to use the logger is at http://www.ruby-doc.org/core/
Also, Ruby documentation can be found at http://www.ruby-lang.org/ including:
* The Learning Ruby (Pickaxe) Book: http://www.ruby-doc.org/docs/ProgrammingRuby/
* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide)
These two online (and free) books will bring you up to speed on the Ruby language
and also on programming in general.
== Debugger
Debugger support is available through the debugger command when you start your Mongrel or
Webrick server with --debugger. This means that you can break out of execution at any point
in the code, investigate and change the model, AND then resume execution! Example:
class WeblogController < ActionController::Base
def index
@posts = Post.find(:all)
debugger
end
end
So the controller will accept the action, run the first line, then present you
with a IRB prompt in the server window. Here you can do things like:
>> @posts.inspect
=> "[#<Post:0x14a6be8 @attributes={\"title\"=>nil, \"body\"=>nil, \"id\"=>\"1\"}>,
#<Post:0x14a6620 @attributes={\"title\"=>\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]"
>> @posts.first.title = "hello from a debugger"
=> "hello from a debugger"
...and even better is that you can examine how your runtime objects actually work:
>> f = @posts.first
=> #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>
>> f.
Display all 152 possibilities? (y or n)
Finally, when you're ready to resume execution, you enter "cont"
== Console
You can interact with the domain model by starting the console through <tt>script/console</tt>.
Here you'll have all parts of the application configured, just like it is when the
application is running. You can inspect domain models, change values, and save to the
database. Starting the script without arguments will launch it in the development environment.
Passing an argument will specify a different environment, like <tt>script/console production</tt>.
To reload your controllers and models after launching the console run <tt>reload!</tt>
== Description of Contents
app
Holds all the code that's specific to this particular application.
app/controllers
Holds controllers that should be named like weblogs_controller.rb for
automated URL mapping. All controllers should descend from ApplicationController
which itself descends from ActionController::Base.
app/models
Holds models that should be named like post.rb.
Most models will descend from ActiveRecord::Base.
app/views
Holds the template files for the view that should be named like
weblogs/index.erb for the WeblogsController#index action. All views use eRuby
syntax.
app/views/layouts
Holds the template files for layouts to be used with views. This models the common
header/footer method of wrapping views. In your views, define a layout using the
<tt>layout :default</tt> and create a file named default.erb. Inside default.erb,
call <% yield %> to render the view using this layout.
app/helpers
Holds view helpers that should be named like weblogs_helper.rb. These are generated
for you automatically when using script/generate for controllers. Helpers can be used to
wrap functionality for your views into methods.
config
Configuration files for the Rails environment, the routing map, the database, and other dependencies.
db
Contains the database schema in schema.rb. db/migrate contains all
the sequence of Migrations for your schema.
doc
This directory is where your application documentation will be stored when generated
using <tt>rake doc:app</tt>
lib
Application specific libraries. Basically, any kind of custom code that doesn't
belong under controllers, models, or helpers. This directory is in the load path.
public
The directory available for the web server. Contains subdirectories for images, stylesheets,
and javascripts. Also contains the dispatchers and the default HTML files. This should be
set as the DOCUMENT_ROOT of your web server.
script
Helper scripts for automation and generation.
test
Unit and functional tests along with fixtures. When using the script/generate scripts, template
test files will be generated for you and placed in this directory.
vendor
External libraries that the application depends on. Also includes the plugins subdirectory.
This directory is in the load path.

View file

@ -0,0 +1,10 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require(File.join(File.dirname(__FILE__), 'config', 'boot'))
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'tasks/rails'

View file

@ -0,0 +1,10 @@
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
# See ActionController::RequestForgeryProtection for details
# Uncomment the :secret if you're not using the cookie session store
protect_from_forgery # :secret => '978f5366084e76bc2bddc16fe1bd2897'
end

View file

@ -0,0 +1,102 @@
require 'campaign/sort.rb'
require 'campaign/campaign.rb'
class CampaignController < ApplicationController
# include lib campaign helper
include CampaignLib
def index
# retrieve elements
@name = params[:name] || nil
@difficulty = params[:diff] || nil
@version = params[:ver] || nil
# now we need to delegate to a proper handler
end
# '/campaign/about?...' handler (version name must be supplied)
def about
# make sure all necessary params are present
if params[:name].nil? || params[:diff].nil? || params[:ver].nil?
redirect_to :controller => 'version', :action => 'index'
return
end
# retrieve version_id, difficulty_id, and campaign_id
version_id = VersionName.find_by_name(params[:ver]).id
# retrieve campaign_id
campaign_id = CampaignName.find_by_name(params[:name]).id
# retrieve difficulty_id
difficulty_id = DifficultyName.find_by_name(params[:diff]).id
# grab all scenarios for the given campaign and apply scenario sorting
scenarios = scenario_sort(CampaignName.all_scenarios_for_campaign(params[:name]))
# create new campaign helper
@obj_campaign = Campaign.new
# store necessary info about this campaign
@obj_campaign.name = params[:name]
@obj_campaign.version = params[:ver]
@obj_campaign.difficulty = params[:diff]
# for every scenario grab all games
scenarios.each do |scenario|
# create new scenario
obj_scenario = Scenario.new
# store name
obj_scenario.name = scenario.name
# grab all games for this scenario, version, difficulty
arr_games = Game.all_games_for_campaign_version_difficulty_and_scenario(campaign_id, version_id, difficulty_id, scenario.id)
# iterate through games and store them in this scenario object
arr_games.each do |game|
# store game
obj_scenario.games << game
# for each game, calculate number of units of each level
arr_game_counts = []
# store counts for this game
obj_scenario.game_unit_counts << Unit.count_unit_levels(game.id)
end
# store scenario
@obj_campaign.scenarios << obj_scenario
end
# compute necessary stats
@obj_campaign.compute_stats
# Wins (across all scenarios)
# Percent turns used on victory (across all scenarios)
# Total minutes per player (across all scenarios)
# Count of Level 1 units at start of game (across all scenarios)
# Count of Level 2 units at start of game (across all scenarios)
# Count of Level 3 units at start of game (across all scenarios)
end
end

View file

@ -0,0 +1,151 @@
require 'version/branch'
class VersionController < ApplicationController
# general '/version' handler
#def index
#
# # redirect to 'list_all'
# redirect_to :action => 'list_all'
#end
# '/version/list_all' handler
# provides a generic listing of branches / versions with links
def index
# reset branch array
@arr_branches = []
# grab a sorted list of all versions
# group versions by branch i.e. (1.4.0 and 1.4.1 are grouped under 1.4)
hash_versions = VersionName.all_branches
# iterate through hash
hash_versions.each do |iter_branch, iter_versions|
# create new branch
obj_branch = Version::Branch.new
# store name of this branch
obj_branch.name = iter_branch
# iterate through all versions in this branch
iter_versions.each do |iter_version|
# create new version
obj_version = Version::Version.new
# store version name
obj_version.name = iter_version.name
# retrieve and store number of games for this version
obj_version.game_count = Game.count_games_for_version(iter_version.id)
#obj_version.game_count = iter_version.games.size
# increment branch game count
obj_branch.game_count += obj_version.game_count
# store version for in this branch
obj_branch.versions << obj_version
end
# index versions in this branch
obj_branch.index_versions_by_subversion
# store branch
@arr_branches << obj_branch
end
end
# '/version/about/<name>' handler (version name must be supplied)
def about
# make sure we have 'name' param present and is valid
if params[:name].nil?
redirect_to :action => 'index'
return
end
# find version with the given name (we need id)
hash_version = VersionName.find_by_name(params[:name])
if hash_version.nil?
redirect_to :action => 'index'
return
end
# make sure we have 'name' is valid/present
#begin
#
# hash_version = VersionNames.find_by_name(params[:name])
#
# logger.info("--")
# logger.info(hash_version)
#
#rescue ActiveRecord::RecordNotFound
#
# # redirect to 'list_all' if missing the 'id'
# redirect_to :action => 'list_all'
# return
#end
# grab a sorted list of all campaigns
hash_campaigns = CampaignName.all_order_by_name
# create new version
@obj_version = Version::Version.new
# store version name
@obj_version.name = hash_version.name
# iterate through hash
hash_campaigns.each do |iter_campaign|
# calculate how many games for this version and this campaign
game_count = Game.count_games_for_campaign_and_version(iter_campaign.id, hash_version.id)
if game_count > 0
# create new campaign
obj_campaign = Version::Campaign.new
# store game count, name
obj_campaign.game_count = game_count
obj_campaign.name = iter_campaign.name
# increment game count for this version
@obj_version.game_count += obj_campaign.game_count
# now we need to retrieve difficulties and game counts for this campaign
hash_difficulties = DifficultyName.all_order_by_name
# iterate through hash
hash_difficulties.each do |iter_difficulty|
# create new difficulty
obj_difficulty = Version::Difficulty.new
# calculate how many games for this version, difficulty and this campaign
game_count = Game.count_games_for_campaign_version_and_difficulty(iter_campaign.id, hash_version.id, iter_difficulty.id)
# store game count, name
obj_difficulty.game_count = game_count
obj_difficulty.name = iter_difficulty.name
# store difficulty for this campaign
obj_campaign.difficulties << obj_difficulty
end
# store this campaign
@obj_version.campaigns << obj_campaign
end
end
end
end

View file

@ -0,0 +1,4 @@
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
end

View file

@ -0,0 +1,3 @@
module CampaignHelper
end

View file

@ -0,0 +1,3 @@
module VersionHelper
end

View file

@ -0,0 +1,33 @@
class CampaignName < ActiveRecord::Base
# there are multiple games for each campaign name
has_many :games
# there are multiple scenarios for this campaign name
has_many :scenario_names, :through => :games
# Return all campaigns, ordered by name
def self.all_order_by_name
CampaignName.find :all, :order => "name ASC"
end
# find all scenarios for a given campaign
def self.all_scenarios_for_campaign(campaign_name)
#CampaignName.find_all_by_name campaign_name,
# :first,
# :select => "scenario_names.name",
# :include => [:scenario_names]
CampaignName.find( :first,
:conditions => ["campaign_names.name = :campaign_name", {:campaign_name => campaign_name}],
:select => "scenario_names.*",
:include => [:scenario_names]).scenario_names
end
end

View file

@ -0,0 +1,10 @@
class DifficultyName < ActiveRecord::Base
# there are multiple games present for each difficulty name
has_many :games
def self.all_order_by_name
DifficultyName.find :all, :order => "name ASC"
end
end

View file

@ -0,0 +1,131 @@
class Game < ActiveRecord::Base
# game has a particular version
belongs_to :version_name
# game has a particular campaign
belongs_to :campaign_name
# game has a particular scenario
belongs_to :scenario_name
# game belongs to a particular difficulty
#belongs_to :difficulty_name#, :class_name => "DifficultyNames", :foreign_key => "difficulty_name_id"
# there are multiple games present for each unit
has_many :units
# there are multiple games present for each special_unit
has_many :special_units
# count how many games of a particular version there are
def self.count_games_for_version(version_id)
Game.count :conditions => ["version_name_id = :version_id", {:version_id => version_id}]
end
# count how many games for a particular campaign there are
def self.count_games_for_campaign(campaign_id)
Game.count :conditions => ["campaign_name_id = :campaign_id", {:campaign_id => campaign_id}]
end
# count how many games for a particular campaign and version there is
def self.count_games_for_campaign_and_version(campaign_id, version_id)
Game.count :conditions => ["campaign_name_id = :campaign_id AND version_name_id = :version_id", {:campaign_id => campaign_id, :version_id => version_id}]
end
# count how many games for a particular campaign, version and difficulty there is
def self.count_games_for_campaign_version_and_difficulty(campaign_id, version_id, difficulty_id)
Game.count :conditions => ["campaign_name_id = :campaign_id AND version_name_id = :version_id AND difficulty_name_id = :difficulty_id", {:campaign_id => campaign_id, :version_id => version_id, :difficulty_id => difficulty_id}]
end
# retrieve all games with given
def self.all_games_for_campaign_version_difficulty_and_scenario(campaign_id, version_id, difficulty_id, scenario_id)
Game.find :all,
:conditions => ["campaign_name_id = :campaign_id AND version_name_id = :version_id AND scenario_name_id = :scenario_id AND difficulty_name_id = :difficulty_id", {:campaign_id => campaign_id, :version_id => version_id, :scenario_id => scenario_id, :difficulty_id => difficulty_id}]
end
# find all unique scenario names for given campaign
#def self.find_scenarios_for_campaign(name)
#
# # CampaignName.find_by_name(params[:name]).scenario_names.uniq
# Game.find :all,
# :select => "DISTINCT scenario_names.name",
# :conditions => ["campaign_names.name = :name AND games.campaign_name_id = campaign_names.id AND games.scenario_name_id = scenario_names.id", {:name => name}],
# :include => [:campaign_name, :scenario_name]
#
#end
#def self.bah(version_id)
#
# Games.find( :all,
# :conditions => ["version_name_id = :version_id", {:version_id => version_id}],
# :select => "version_names.id, games.id",
# :include => [:version_name]#[:campaign_name, :version_name]
# )
#
#end
#
#def self.find_games_for_version(version_id)
#
# Games.find( :all,
# :conditions => ["version_name_id = :version_id", {:version_id => version_id}],
# :select => "
# )
#
#end
# count how many games for each version there is
#def self.count_games_for_each_version
#
# #Games.count(
# # :joins => "JOIN version_names on version_name_id = version_names.id",
# # :group => "version_name_id",
# # :order => "version_names.name"
# # )
#
# Games.find( :all,
# :joins => "JOIN version_names on version_name_id = version_names.id",
# #:select => "version_names.id, version_names.name, COUNT(*) as 'game_count'",
# :select => "version_names.name, COUNT(*) as 'game_count'",
# :group => "version_name_id",
# :order => "version_names.name"
# #:offset => offset,
# #:limit => limit
# )
#end
# count how many games for each branch there is
#def self.count_games_for_branch(branch)
#
# branch_query = branch + "%"
#
# Games.find( :all,
# :conditions => ["version_names.name LIKE :branch_query", {:branch_query => branch_query}],
# :joins => "JOIN version_names on version_name_id = version_names.id",
# :select => "COUNT(*) as 'game_count'"
# #:group => "version_name_id",
# #:order => "version_names.name"
# )
#
#end
end

View file

@ -0,0 +1,2 @@
class Player < ActiveRecord::Base
end

View file

@ -0,0 +1,6 @@
class ScenarioName < ActiveRecord::Base
# there are multiple games for each scenario
has_many :games
end

View file

@ -0,0 +1,9 @@
class SpecialUnit < ActiveRecord::Base
# special unit belongs to a particular game
belongs_to :game
# special unit belongs to a particular special_unit_name
belongs_to :special_unit_name
end

View file

@ -0,0 +1,5 @@
class SpecialUnitName < ActiveRecord::Base
# there are multiple special units for each special_unit_name
has_many :special_units
end

View file

@ -0,0 +1,25 @@
class Unit < ActiveRecord::Base
# unit belongs to a particular game
belongs_to :game
# unit belongs to a particular unit_name
belongs_to :unit_name
# count how many units of a given level there are for a given game
#def self.count_units_of_level(game_id, level)
#
# Unit.sum :count,
# :conditions => ["game_id = :game_id AND level = :level", {:game_id => game_id, :level => level}]
#end
# return unit counts for each level
def self.count_unit_levels(game_id)
Unit.find :all,
:select => "level, SUM(count) AS level_count",
:conditions => ["game_id = :game_id", {:game_id => game_id}],
:group => 'level'
end
end

View file

@ -0,0 +1,5 @@
class UnitName < ActiveRecord::Base
# there are multiple units for each unit_name
has_many :units
end

View file

@ -0,0 +1,27 @@
class VersionName < ActiveRecord::Base
# there are multiple games for each version
has_many :games
# count how many different versions there are
def self.count_versions
VersionName.count
end
# return all branches
def self.all_branches
VersionName.all_order_by_name.group_by { |version| version.name[0, 3] }
#VersionName.all.group_by { |version| version.name[0, 3] }
end
# return all versions ordered by name
def self.all_order_by_name
VersionName.find :all, :order => "name DESC"
end
end

View file

@ -0,0 +1,2 @@
%img{ :src => count_units_level_1 }
%p

View file

@ -0,0 +1,2 @@
%img{ :src => count_units_level_2 }
%p

View file

@ -0,0 +1,2 @@
%img{ :src => count_units_level_3 }
%p

View file

@ -0,0 +1,2 @@
%img{ :src => gold_start_game }
%p

View file

@ -0,0 +1,2 @@
%img{ :src => victory_percent_turns }
%p

View file

@ -0,0 +1,2 @@
%img{ :src => wins_losses_quits }
%p

View file

@ -0,0 +1,42 @@
%center
Campaign:
= @obj_campaign.name
Difficulty:
= @obj_campaign.difficulty
Version:
= @obj_campaign.version
%p
Gold at start of game:
%p
%p
= render :partial => 'gold_start_game', :collection => @obj_campaign.create_charts_gold_start_game
Wins, Losses, Quits percentage:
%p
%p
= render :partial => 'wins_losses_quits', :collection => @obj_campaign.create_charts_wins_losses_quits
Percent turns used on victory:
%p
%p
= render :partial => 'victory_percent_turns', :collection => @obj_campaign.create_charts_victory_percent_turns
Count of level 1 units:
%p
%p
= render :partial => 'count_units_level_1', :collection => @obj_campaign.create_charts_unit_count(1)
Count of level 2 units:
%p
%p
= render :partial => 'count_units_level_2', :collection => @obj_campaign.create_charts_unit_count(2)
Count of level 3 units:
%p
%p
= render :partial => 'count_units_level_3', :collection => @obj_campaign.create_charts_unit_count(3)

View file

@ -0,0 +1,13 @@
Branch:
= branch.name
Number of games:
= branch.game_count
%p
= render :partial => 'branch_version', :collection => branch.versions
%img{ :src => branch.create_branch_chart }/
%p

View file

@ -0,0 +1,9 @@
Version:
= branch_version.name
Number of games:
= branch_version.game_count
Link:
= link_to "version_link", {:action => "about", :name => branch_version.name}
%p

View file

@ -0,0 +1,15 @@
Campaign:
= campaign.name
Number of games:
= campaign.game_count
/Link:
/= link_to "campaign_link", {:controller => "campaign", :action => "about", :name => campaign.name, :diff => }
%p
= render :partial => 'campaign_difficulty', :collection => campaign.difficulties, :locals => { :campaign_name => campaign.name, :version_name => version_name}
%img{ :src => campaign.create_difficulty_chart }/
%p

View file

@ -0,0 +1,10 @@
Difficulty:
= campaign_difficulty.name
Number of games:
= campaign_difficulty.game_count
Link:
= link_to "campaign_link", {:controller => "campaign", :action => "about", :name => campaign_name, :diff => campaign_difficulty.name, :ver => version_name}
%p

View file

@ -0,0 +1,9 @@
Version:
= @obj_version.name
Game count:
= @obj_version.game_count
%p
.branch-section= render :partial => 'campaign', :collection => @obj_version.campaigns, :locals => { :version_name => @obj_version.name }

View file

@ -0,0 +1,2 @@
.branch-section= render :partial => 'branch', :collection => @arr_branches

View file

@ -0,0 +1,109 @@
# Don't change this file!
# Configure your app in config/environment.rb and config/environments/*.rb
RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
module Rails
class << self
def boot!
unless booted?
preinitialize
pick_boot.run
end
end
def booted?
defined? Rails::Initializer
end
def pick_boot
(vendor_rails? ? VendorBoot : GemBoot).new
end
def vendor_rails?
File.exist?("#{RAILS_ROOT}/vendor/rails")
end
# FIXME : Ruby 1.9
def preinitialize
load(preinitializer_path) if File.exists?(preinitializer_path)
end
def preinitializer_path
"#{RAILS_ROOT}/config/preinitializer.rb"
end
end
class Boot
def run
load_initializer
Rails::Initializer.run(:set_load_path)
end
end
class VendorBoot < Boot
def load_initializer
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
end
end
class GemBoot < Boot
def load_initializer
self.class.load_rubygems
load_rails_gem
require 'initializer'
end
def load_rails_gem
if version = self.class.gem_version
gem 'rails', version
else
gem 'rails'
end
rescue Gem::LoadError => load_error
$stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
exit 1
end
class << self
def rubygems_version
Gem::RubyGemsVersion if defined? Gem::RubyGemsVersion
end
def gem_version
if defined? RAILS_GEM_VERSION
RAILS_GEM_VERSION
elsif ENV.include?('RAILS_GEM_VERSION')
ENV['RAILS_GEM_VERSION']
else
parse_gem_version(read_environment_rb)
end
end
def load_rubygems
require 'rubygems'
unless rubygems_version >= '0.9.4'
$stderr.puts %(Rails requires RubyGems >= 0.9.4 (you have #{rubygems_version}). Please `gem update --system` and try again.)
exit 1
end
rescue LoadError
$stderr.puts %(Rails requires RubyGems >= 0.9.4. Please install RubyGems and try again: http://rubygems.rubyforge.org)
exit 1
end
def parse_gem_version(text)
$1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
end
private
def read_environment_rb
File.read("#{RAILS_ROOT}/config/environment.rb")
end
end
end
end
# All that for this:
Rails.boot!

View file

@ -0,0 +1,27 @@
development:
adapter: mysql
database: wesnoth
host: <my_host_name>
port: 3306
username: <my_user_name>
password: <my_pswd>
timeout: 5000
test:
adapter: mysql
database: wesnoth
host: <my_host_name>
port: 3306
username: <my_user_name>
password: <my_pswd>
timeout: 5000
production:
adapter: mysql
database: wesnoth
host: <my_host_name>
port: 3306
username: <my_user_name>
password: <my_pswd>
timeout: 5000

View file

@ -0,0 +1,62 @@
# Be sure to restart your server when you modify this file
# Uncomment below to force Rails into production mode when
# you don't control web/app server and can't set it the proper way
ENV['RAILS_ENV'] ||= 'production'
# YOU MUST BE IN PRODUCTION MODE AT ALL TIMES
# Specifies gem version of Rails to use when vendor/rails is not present
RAILS_GEM_VERSION = '2.1' unless defined? RAILS_GEM_VERSION
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
require 'gchart'
Rails::Initializer.run do |config|
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
# See Rails::Configuration for more options.
# Skip frameworks you're not going to use (only works if using vendor/rails).
# To use Rails without a database, you must remove the Active Record framework
# config.frameworks -= [ :active_record, :active_resource, :action_mailer ]
# Only load the plugins named here, in the order given. By default, all plugins
# in vendor/plugins are loaded in alphabetical order.
# :all can be used as a placeholder for all plugins not explicitly named
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
# Add additional load paths for your own custom dirs
# config.load_paths += %W( #{RAILS_ROOT}/extras )
# Force all environments to use the same logger level
# (by default production uses :info, the others :debug)
# config.log_level = :debug
# Your secret key for verifying cookie session data integrity.
# If you change this key, all old sessions will become invalid!
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
config.action_controller.session = {
:session_key => '_wesnoth_session',
:secret => '8da8b64e598afc43369c14b4abdd5ee3942228eab2c41ab47549c89b543f69d6ef6a0ab17d85062beb6c42bc7c9d9bc59a640016695ed0bceff8cf41b747d28f'
}
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
# (create the session table with 'rake db:sessions:create')
# config.action_controller.session_store = :active_record_store
# Use SQL instead of Active Record's schema dumper when creating the test database.
# This is necessary if your schema can't be completely dumped by the schema dumper,
# like if you have constraints or database-specific column types
# config.active_record.schema_format = :sql
# Activate observers that should always be running
# config.active_record.observers = :cacher, :garbage_collector
# Make Active Record use UTC-base instead of local time
# config.active_record.default_timezone = :utc
end

View file

@ -0,0 +1,18 @@
# Settings specified here will take precedence over those in config/environment.rb
# 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.
config.cache_classes = false
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_view.debug_rjs = true
config.action_controller.perform_caching = false
config.action_view.cache_template_extensions = false
# Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = false

View file

@ -0,0 +1,19 @@
# Settings specified here will take precedence over those in config/environment.rb
# The production environment is meant for finished, "live" apps.
# Code is not reloaded between requests
config.cache_classes = true
# Use a different logger for distributed setups
# config.logger = SyslogLogger.new
# Full error reports are disabled and caching is turned on
config.action_controller.consider_all_requests_local = false
config.action_controller.perform_caching = true
config.action_view.cache_template_loading = true
# Enable serving of images, stylesheets, and javascripts from an asset server
# config.action_controller.asset_host = "http://assets.example.com"
# Disable delivery errors, bad email addresses will be ignored
# config.action_mailer.raise_delivery_errors = false

View file

@ -0,0 +1,22 @@
# Settings specified here will take precedence over those in config/environment.rb
# 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!
config.cache_classes = true
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Disable request forgery protection in test environment
config.action_controller.allow_forgery_protection = false
# Tell ActionMailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test

View file

@ -0,0 +1,10 @@
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format
# (all these examples are active by default):
# Inflector.inflections do |inflect|
# inflect.plural /^(ox)$/i, '\1en'
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end

View file

@ -0,0 +1,5 @@
# Be sure to restart your server when you modify this file.
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf
# Mime::Type.register_alias "text/html", :iphone

View file

@ -0,0 +1,15 @@
# These settins change the behavior of Rails 2 apps and will be defaults
# for Rails 3. You can remove this initializer when Rails 3 is released.
# Only save the attributes that have changed since the record was loaded.
ActiveRecord::Base.partial_updates = true
# Include ActiveRecord class name as root for JSON serialized output.
ActiveRecord::Base.include_root_in_json = true
# Use ISO 8601 format for JSON serialized times and dates
ActiveSupport.use_standard_json_time_format = true
# Don't escape HTML entities in JSON, leave that for the #json_escape helper
# if you're including raw json in an HTML page.
ActiveSupport.escape_html_entities_in_json = false

View file

@ -0,0 +1,42 @@
ActionController::Routing::Routes.draw do |map|
# The priority is based upon order of creation: first created -> highest priority.
# Sample of regular route:
# map.connect 'products/:id', :controller => 'catalog', :action => 'view'
# Keep in mind you can assign values other than :controller and :action
# Sample of named route:
# map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase'
# This route can be invoked with purchase_url(:id => product.id)
# Sample resource route (maps HTTP verbs to controller actions automatically):
# map.resources :products
# Sample resource route with options:
# map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get }
# Sample resource route with sub-resources:
# map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller
# Sample resource route within a namespace:
# map.namespace :admin do |admin|
# # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb)
# admin.resources :products
# end
# You can have the root of your site routed with map.root -- just remember to delete public/index.html.
# map.root :controller => "welcome"
# See how all your routes lay out with "rake routes"
# Install the default routes as the lowest priority.
# Routing for version
map.connect 'version', :controller => 'version'
map.connect 'version/about/:name', :controller => 'version', :action => 'about', :requirements => { :name => /.*/ }
# Default routing
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
end

View file

@ -0,0 +1,18 @@
class CreateCampaignNames < ActiveRecord::Migration
def self.up
create_table :campaign_names do |t|
t.column :name, :string
end
# switch to 'MyISAM'
execute 'ALTER TABLE campaign_names ENGINE = MyISAM'
end
def self.down
drop_table :campaign_names
end
end

View file

@ -0,0 +1,18 @@
class CreateDifficultyNames < ActiveRecord::Migration
def self.up
create_table :difficulty_names do |t|
t.column :name, :string
end
# switch to 'MyISAM'
execute 'ALTER TABLE difficulty_names ENGINE = MyISAM'
end
def self.down
drop_table :difficulty_names
end
end

View file

@ -0,0 +1,18 @@
class CreateScenarioNames < ActiveRecord::Migration
def self.up
create_table :scenario_names do |t|
t.column :name, :string
end
# switch to 'MyISAM'
execute 'ALTER TABLE scenario_names ENGINE = MyISAM'
end
def self.down
drop_table :scenario_names
end
end

View file

@ -0,0 +1,18 @@
class CreatePlayers < ActiveRecord::Migration
def self.up
create_table :players do |t|
t.column :player_id, :integer
end
# switch to 'MyISAM'
execute 'ALTER TABLE players ENGINE = MyISAM'
end
def self.down
drop_table :players
end
end

View file

@ -0,0 +1,21 @@
class CreateSpecialUnits < ActiveRecord::Migration
def self.up
create_table :special_units do |t|
t.column :game_id, :integer
t.column :level, :integer
t.column :special_unit_id, :integer
t.column :experience, :integer
end
# switch to 'MyISAM'
execute 'ALTER TABLE special_units ENGINE = MyISAM'
end
def self.down
drop_table :special_units
end
end

View file

@ -0,0 +1,18 @@
class CreateSpecialUnitNames < ActiveRecord::Migration
def self.up
create_table :special_unit_names do |t|
t.column :name, :string
end
# switch to 'MyISAM'
execute 'ALTER TABLE special_unit_names ENGINE = MyISAM'
end
def self.down
drop_table :special_unit_names
end
end

View file

@ -0,0 +1,21 @@
class CreateUnits < ActiveRecord::Migration
def self.up
create_table :units do |t|
t.column :game_id, :integer
t.column :level, :integer
t.column :unit_id, :integer
t.column :count, :integer
end
# switch to 'MyISAM'
execute 'ALTER TABLE units ENGINE = MyISAM'
end
def self.down
drop_table :units
end
end

View file

@ -0,0 +1,18 @@
class CreateUnitNames < ActiveRecord::Migration
def self.up
create_table :unit_names do |t|
t.column :name, :string
end
# switch to 'MyISAM'
execute 'ALTER TABLE unit_names ENGINE = MyISAM'
end
def self.down
drop_table :unit_names
end
end

View file

@ -0,0 +1,18 @@
class CreateVersionNames < ActiveRecord::Migration
def self.up
create_table :version_names do |t|
t.column :name, :string
end
# switch to 'MyISAM'
execute 'ALTER TABLE version_names ENGINE = MyISAM'
end
def self.down
drop_table :version_names
end
end

View file

@ -0,0 +1,31 @@
class CreateGames < ActiveRecord::Migration
def self.up
create_table :games do |t|
t.column :player_id, :integer
t.column :version_id, :integer
t.column :campaign_id, :integer
t.column :scenario_id, :integer
t.column :difficulty_id, :integer
t.column :number_turns, :integer
t.column :start_turn, :integer
t.column :end_turn, :integer
t.column :start_time, :integer
t.column :end_time, :integer
t.column :gold, :integer
t.column :end_gold, :integer
t.column :status, :integer
t.column :serial, :string
end
# switch to 'MyISAM'
execute 'ALTER TABLE games ENGINE = MyISAM'
end
def self.down
drop_table :games
end
end

View file

@ -0,0 +1,19 @@
class ModifyPlayersPlayerId < ActiveRecord::Migration
def self.up
# fix a bug, player_id is supposed to be a string
# also rename it, since it no longer will be used in indexing
change_column :players, :player_id, :string
rename_column :players, :player_id, :unique_player_id
end
def self.down
# can't go back
raise ActiveRecord::IrreversibleMigration
end
end

View file

@ -0,0 +1,15 @@
class ModifyPlayersUniqueIdLength < ActiveRecord::Migration
def self.up
# make unique_id max length equal to 30
change_column :players, :unique_player_id, :string, :limit => 30
end
def self.down
# remove unique_id length limit
change_column :players, :unique_player_id, :string
end
end

View file

@ -0,0 +1,15 @@
class ModifyGamesSerialLength < ActiveRecord::Migration
def self.up
# make serial max length equal to 30
change_column :games, :serial, :string, :limit => 30
end
def self.down
# remove serial length limit
change_column :games, :serial, :string
end
end

View file

@ -0,0 +1,16 @@
class ModifyGamesVersionNameId < ActiveRecord::Migration
def self.up
# rename version_id to version_name_id
rename_column :games, :version_id, :version_name_id
end
def self.down
# rename version_name_id to version_id
rename_column :games, :version_name_id, :version_id
end
end

View file

@ -0,0 +1,16 @@
class ModifyGamesCampaignNameId < ActiveRecord::Migration
def self.up
# rename campaign_id to campaign_name_id
rename_column :games, :campaign_id, :campaign_name_id
end
def self.down
# rename campaign_name_id to campaign_id
rename_column :games, :campaign_name_id, :campaign_id
end
end

View file

@ -0,0 +1,16 @@
class ModifyGamesScenarioNameId < ActiveRecord::Migration
def self.up
# rename scenario_id to scenario_name_id
rename_column :games, :scenario_id, :scenario_name_id
end
def self.down
# rename scenario_name_id to scenario_id
rename_column :games, :scenario_name_id, :scenario_id
end
end

View file

@ -0,0 +1,15 @@
class ModifyGamesDifficultyNameId < ActiveRecord::Migration
def self.up
# rename difficulty_id to difficulty_name_id
rename_column :games, :difficulty_id, :difficulty_name_id
end
def self.down
# rename difficulty_name_id to difficulty_id
rename_column :games, :difficulty_name_id, :difficulty_id
end
end

View file

@ -0,0 +1,15 @@
class ModifyUnitsUnitNameId < ActiveRecord::Migration
def self.up
# rename unit_id to unit_name_id
rename_column :units, :unit_id, :unit_name_id
end
def self.down
# rename unit_name_id to unit_id
rename_column :units, :unit_name_id, :unit_id
end
end

View file

@ -0,0 +1,15 @@
class ModifySpecialUnitsSpecialUnitNameId < ActiveRecord::Migration
def self.up
# rename special_unit_id to special_unit_name_id
rename_column :special_units, :special_unit_id, :special_unit_name_id
end
def self.down
# rename special_unit_name_id to special_unit_id
rename_column :special_units, :special_unit_name_id, :special_unit_id
end
end

View file

@ -0,0 +1,73 @@
# This file is auto-generated from the current state of the database. Instead of editing this file,
# please use the migrations feature of Active Record to incrementally modify your database, and
# then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your database schema. If you need
# to create the application database on another system, you should be using db:schema:load, not running
# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 19) do
create_table "campaign_names", :force => true do |t|
t.string "name"
end
create_table "difficulty_names", :force => true do |t|
t.string "name"
end
create_table "games", :force => true do |t|
t.integer "player_id", :limit => 11
t.integer "version_name_id", :limit => 11
t.integer "campaign_name_id", :limit => 11
t.integer "scenario_name_id", :limit => 11
t.integer "difficulty_name_id", :limit => 11
t.integer "number_turns", :limit => 11
t.integer "start_turn", :limit => 11
t.integer "end_turn", :limit => 11
t.integer "start_time", :limit => 11
t.integer "end_time", :limit => 11
t.integer "gold", :limit => 11
t.integer "end_gold", :limit => 11
t.integer "status", :limit => 11
t.string "serial", :limit => 30
end
create_table "players", :force => true do |t|
t.string "unique_player_id", :limit => 30
end
create_table "scenario_names", :force => true do |t|
t.string "name"
end
create_table "special_unit_names", :force => true do |t|
t.string "name"
end
create_table "special_units", :force => true do |t|
t.integer "game_id", :limit => 11
t.integer "level", :limit => 11
t.integer "special_unit_name_id", :limit => 11
t.integer "experience", :limit => 11
end
create_table "unit_names", :force => true do |t|
t.string "name"
end
create_table "units", :force => true do |t|
t.integer "game_id", :limit => 11
t.integer "level", :limit => 11
t.integer "unit_name_id", :limit => 11
t.integer "count", :limit => 11
end
create_table "version_names", :force => true do |t|
t.string "name"
end
end

View file

@ -0,0 +1,2 @@
Use this README file to introduce your application and point to useful places in the API for learning more.
Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries.

View file

@ -0,0 +1,317 @@
require 'campaign/scenario'
require 'googlechart/chart_bar'
require 'googlechart/chart_xy'
module CampaignLib
include GoogleChartLib
# Represents a campaign instance, with a set of scenarios
class Campaign
attr_accessor :scenarios, :name, :difficulty, :version
def initialize
@scenarios = []
@scenario_blocks = []
@name = ""
@difficulty = ""
@version = ""
end
# compute stats for this campaign
def compute_stats
# compute stats for each scenario
@scenarios.each do |scenario|
# compute gold at the start of the game (across all scenarios)
scenario.compute_start_gold
# compute Losses/quits (across all scenarios)
scenario.compute_wins_losses_quits
# compute victory percent turns (across all scenarios)
scenario.compute_victory_percent_turns
# compute unit counts
scenario.compute_unit_counts
end
# partition scenarios into blocks, so we can generate charts for each block
create_scenario_blocks
end
# create chart for unit count (given level)
def create_charts_unit_count(level)
# we are going to generate an array of charts showing
# unit counts for the given level
arr_charts = []
# for each block generate a chart
@scenario_blocks.each do |scenario_block|
arr_data = []
sym_level = "level_#{level}".to_sym
# store means and medians
arr_data << scenario_block.map { |scenario| scenario.stat_unit_counts[sym_level].mean }
arr_data << scenario_block.map { |scenario| scenario.stat_unit_counts[sym_level].median }
# store names
arr_labels = []
scenario_block.each do |scenario|
if scenario.name.length > 20
arr_labels << scenario.name[0, 20] + ".."
else
arr_labels << scenario.name
end
end
#scenario_block.map { |scenario| scenario.name[0, 20] }
obj_chart = chart_xy :title => "Count of level #{level} units: #{arr_labels.first} - #{arr_labels.last}",
:size => [700, 200],
:labels => arr_labels,
:data => arr_data,
:legend => ['Mean', 'Median'],
:colors => ['0033FF', 'FF6600'],
:legend_position => :top
# store chart (unit counts), median and mode
arr_charts << obj_chart
arr_data = []
# store max and mode
arr_data << scenario_block.map { |scenario| scenario.stat_unit_counts[sym_level].max }
arr_data << scenario_block.map { |scenario| scenario.stat_unit_counts[sym_level].mode }
obj_chart = chart_xy :title => "Count of level #{level} units: #{arr_labels.first} - #{arr_labels.last}",
:size => [700, 200],
:labels => arr_labels,
:data => arr_data,
:legend => ['Maximum', 'Mode'],
:colors => ['990000', '007700'],
:legend_position => :top
# store chart (unit counts), median and mode
arr_charts << obj_chart
end
return arr_charts
end
# create chart for number of turns % wins
def create_charts_victory_percent_turns
# we are going to generate an array of charts showing
# victory percent turns
arr_charts = []
# for each block generate a chart
@scenario_blocks.each do |scenario_block|
arr_data = []
# store means and medians
arr_data << scenario_block.map { |scenario| scenario.victory_percent_turns.mean }
arr_data << scenario_block.map { |scenario| scenario.victory_percent_turns.median }
# store names
#arr_labels = scenario_block.map { |scenario| scenario.name[0, 20] }
# store names
arr_labels = []
scenario_block.each do |scenario|
if scenario.name.length > 20
arr_labels << scenario.name[0, 20] + ".."
else
arr_labels << scenario.name
end
end
# create chart
obj_chart = chart_xy :title => "Percent turns used on victory: #{arr_labels.first} - #{arr_labels.last}",
:size => [700, 200],
:labels => arr_labels,
:data => arr_data,
:legend => ['Mean', 'Median'],
:colors => ['006400', 'FF6600'],
:legend_position => :top
# store chart
arr_charts << obj_chart
end
return arr_charts
end
# create chart for gold at the start of the game
def create_charts_gold_start_game
# we are going to generate an array of charts showing
# gold at the start of the game
arr_charts = []
# for each block (batch) generate a chart
@scenario_blocks.each do |scenario_block|
arr_data = []
arr_data << scenario_block.map { |scenario| scenario.gold_start_game.mean }
arr_data << scenario_block.map { |scenario| scenario.gold_start_game.median }
#arr_labels = scenario_block.map { |scenario| scenario.name[0, 20] }
# store names
arr_labels = []
scenario_block.each do |scenario|
if scenario.name.length > 20
arr_labels << scenario.name[0, 20] + ".."
else
arr_labels << scenario.name
end
end
# create chart
obj_chart = chart_xy :data => arr_data,
:labels => arr_labels,
:size => [700, 200],
:colors => ['FF0000', '0000FF'],
:title => "Gold at start of the game: #{arr_labels.first} - #{arr_labels.last}",
:legend => ['Mean', 'Median'],
:legend_position => :top
# store chart
arr_charts << obj_chart
end
# return all charts for the given campaign
return arr_charts
end
# create chart for wins/losses/quits
def create_charts_wins_losses_quits
arr_charts = []
# for each block (batch) generate a chart
@scenario_blocks.each do |scenario_block|
arr_data = []
arr_data << scenario_block.map { |scenario| scenario.status_wins.percentage }
arr_data << scenario_block.map { |scenario| scenario.status_losses.percentage }
arr_data << scenario_block.map { |scenario| scenario.status_quits.percentage }
#arr_labels = scenario_block.map { |scenario| scenario.name[0, 20] }
# store names
arr_labels = []
scenario_block.each do |scenario|
if scenario.name.length > 20
arr_labels << scenario.name[0, 20] + ".."
else
arr_labels << scenario.name
end
end
# create chart
obj_chart = chart_bar :title => "Wins, Losses, Quits percentage: #{arr_labels.first} - #{arr_labels.last}",
:data => arr_data,
:size => [700 * scenario_block.size / 4, 250],
:grouped => true,
:width_spacing => [30, 5, 50],
:colors => ['006400', 'CC1100', 'EEAD0E'],
:legend => ['Wins %', 'Losses %', 'Quits %'],
:legend_position => :top,
:grid => [100, 10],
:axis_labels => [
{:axis => :y, :labels => Array.new(11) {|i| (i * 10).to_s}},
{:axis => :x, :labels => arr_labels}
]
# store chart
arr_charts << obj_chart
end
# return all charts for the given campaign
return arr_charts
end
# partition list of scenarios into blocks
# this helps to avoid data cluttering (create a series of charts)
private
def create_scenario_blocks
@scenario_blocks = []
arr_current_block = []
obj_trans = nil
pos = 0
@scenarios.each do |scenario|
if pos > 0 && 0 == pos % 3
arr_current_block << scenario
# store previous batch
@scenario_blocks << arr_current_block
# create new batch
arr_current_block = []
# store border element
if scenario != @scenarios.last
arr_current_block << scenario
end
else
# store scenario (current batch)
arr_current_block << scenario
end
pos += 1
end
# store last batch
unless arr_current_block.empty?
@scenario_blocks << arr_current_block
end
end
end
end

View file

@ -0,0 +1,103 @@
require 'campaign/stats.rb'
module CampaignLib
# Represents a scenario instance (for particular version, difficulty)
# Has a set of games played associated with it
class Scenario
attr_accessor :games, :game_unit_counts
attr_accessor :name
attr_accessor :gold_start_game
attr_accessor :status_wins, :status_losses, :status_quits
attr_accessor :victory_percent_turns
attr_accessor :stat_unit_counts
def initialize
@name = nil
@games = []
@gold_start_game = Stats.new
@status_wins = Stats.new
@status_losses = Stats.new
@status_quits = Stats.new
@victory_percent_turns = Stats.new
@game_unit_counts = []
@stat_unit_counts = {}
3.times do |level|
@stat_unit_counts["level_#{level + 1}".to_sym] = Stats.new
end
end
# compute unit counts
def compute_unit_counts
# compute stat unit counts for each level
3.times do |level|
#@stat_unit_counts["level_#{level + 1}".to_sym].compute( game_unit_counts.map { |level_counts| level_counts[level] } )
arr_counts = []
level_symbol = "level_#{level + 1}".to_sym
@game_unit_counts.each do |game_unit_counts|
game_unit_counts.each do |level_counts|
# proper level
if level_counts.level.to_i == level + 1
arr_counts << level_counts.level_count.to_i
end
end
end
@stat_unit_counts[level_symbol].compute( arr_counts )
end
end
# compute start gold
def compute_start_gold
# compute gold at the start of the game
@gold_start_game.compute( @games.map { |game| game.gold } )
end
# compute losses/quits
def compute_wins_losses_quits
@status_wins.compute_counts( @games.select { |game| game.status.to_i == 1 }, @games.size )
@status_losses.compute_counts( @games.select { |game| game.status.to_i == 2}, @games.size )
@status_quits.compute_counts( @games.select { |game| game.status.to_i == 0}, @games.size )
end
# compuet percent turns used on victory
def compute_victory_percent_turns
arr_ratios = []
@games.each do |game|
# only interested in wins (and not interested in games with unlimited or 0 # of turns)
if game.status.to_i == 1 && game.number_turns.to_i > 0
arr_ratios << ((game.end_turn.to_f / game.number_turns.to_f) * 100).to_i
end
end
@victory_percent_turns.compute(arr_ratios)
end
end
end

View file

@ -0,0 +1,124 @@
module CampaignLib
# return minimum of two numbers (built-in min is confusing)
def min(a, b)
a <= b ? a : b
end
# return maximum of two numbers (built-in max is confusing)
def self.max(a, b)
a >= b ? a : b
end
# return the type of token
def get_token_type(token)
if token =~ /\d+[a-zA-Z]*/
# (number followed by letters)
:token_number
else
# everything else
:token_alpha
end
end
# helper comparison method (emulating <=> operator)
def compare(left, right)
# do smallest number of times
min(left.size, right.size).times do |i|
# get token types
type_left = get_token_type(left[i].first)
type_right = get_token_type(right[i].first)
int_left = left[i].first.to_i
int_right = right[i].first.to_i
str_left = left[i].first.downcase
str_right = right[i].first.downcase
# check if types are equal
if type_left == type_right
# if both start with number
if :token_number == type_left
if int_left == int_right
# compare strings
if str_left == str_right
# if match, skip to next
next
end
# not equal, return string based
return str_left <=> str_right
else
# if not, return int comparison
return int_left <=> int_right
end
else
# compare strings
if str_left == str_right
# if match, skip to next
next
end
#both are words, we compare strings
return str_left <=> str_right
end
else
if :token_alpha == type_right
# no need swapping
return 1
else
# need to swap
return -1
end
end
end
return 0
end
# sort scenarios (we'll try to sort based on number comparison and not string)
def scenario_sort(arr)
# destructively sort the collection
arr.sort! do |left, right|
# tokenize left and right params
arr_left = left.name.scan /([^_]+)/
arr_right = right.name.scan /([^_]+)/
# compare tokenized arrays
# This will yield order like this:
# "Drake_Council"
# 1_Epilogue
# 1_3_Out_of_the_Swamps
# 1_4t_Berdssenhold
# 2_2_South_Across_the_Plains
compare(arr_left, arr_right)
end
end
end

View file

@ -0,0 +1,84 @@
module CampaignLib
# Represents statistics information regarding certain aspect
class Stats
attr_accessor :mean, :median, :mode, :min, :max, :percentage
def initialize
@mean = 0
@median = 0
@mode = 0
@min = 0
@max = 0
@percentage = 0
end
# compute % count on the given set
def compute_counts(data, set_size)
if set_size > 0
@percentage = ((data.size.to_f / set_size.to_f) * 100).to_i
end
end
# compute mean, median, min, max, mode on the given set
def compute(data)
# compute only if we have data
if data.size > 0
# first sort the data
data.sort!
# frequency
frequency_table = {}
frequency_max = 0
# go through data and compute necessary stats
data.each do |element|
el_value = element.to_i
# check min max
if el_value > @max
@max = el_value
end
if el_value < @min
@min = el_value
end
# computing mean
@mean += el_value
# computing mode
frequency_table[element] ||= 0
frequency_table[element] += 1
if frequency_table[element] > frequency_max
# calculating element that repeats the most
frequency_max = frequency_table[element]
@mode = element
end
end
# compute mean
@mean = @mean / data.size
# compute median
@median = data[data.size / 2]
end
end
end
end

View file

@ -0,0 +1,839 @@
#require 'extensions/enumerable'
module GoogleChartLib
class Chart
# allowed args for each chart type
@@allowed_args = {}
# accessors
attr_reader :url
attr_accessor :type
attr_accessor :colors, :size, :data, :data_encoding
attr_accessor :title, :title_color, :title_fontsize
attr_accessor :legend, :legend_position
attr_accessor :orientation, :grouped
attr_accessor :labels
attr_accessor :axis_labels, :grid
attr_accessor :zero_line, :width_spacing
attr_accessor :markers
# helper, registers allowed arguments for each chart type
private
def self.register_type_args(type, args)
if @@allowed_args[type].nil?
@@allowed_args[type] = []
end
@@allowed_args[type] += args
end
# register allowed arguments for each
def self.register
# register bar chart arguments
register_type_args(:bar, [:colors, :size, :data, :data_encoding])
register_type_args(:bar, [:title, :title_color, :title_fontsize])
register_type_args(:bar, [:legend, :legend_position])
register_type_args(:bar, [:orientation, :grouped])
register_type_args(:bar, [:axis_labels])
register_type_args(:bar, [:zero_line, :width_spacing])
register_type_args(:bar, [:axis_labels, :grid, :markers])
# register line chart arguments
register_type_args(:line, [:colors, :size, :data, :data_encoding])
register_type_args(:line, [:title, :title_color, :title_fontsize])
register_type_args(:line, [:legend, :legend_position])
register_type_args(:line, [:axis_labels, :grid, :markers])
# register line chart arguments
register_type_args(:line_xy, [:colors, :size, :data, :data_encoding])
register_type_args(:line_xy, [:title, :title_color, :title_fontsize])
register_type_args(:line_xy, [:legend, :legend_position])
register_type_args(:line_xy, [:axis_labels, :grid, :markers])
# register pie chart arguments
register_type_args(:pie, [:colors, :size, :data, :data_encoding])
register_type_args(:pie, [:title, :title_color, :title_fontsize])
register_type_args(:pie, [:labels])
# register pie 3d chart arguments
register_type_args(:pie3d, [:colors, :size, :data, :data_encoding])
register_type_args(:pie3d, [:title, :title_color, :title_fontsize])
register_type_args(:pie3d, [:labels])
end
# invoke registration
register
# helper, used to filter out unsupported args
def self.filter(args, allowed_args)
# go through all arguments and check which ones apply
args.each do |key|
# check if arg is supported
unless allowed_args.include? key
# not supported - delete it
args.delete(key)
end
end
return args
end
# handle missing calls
public
def self.method_missing(method, args = {})
# check if this is a valid chart
unless @@allowed_args[method].nil?
# filter unsupported args
valid_args = filter(args, @@allowed_args[method])
# insert type arg
valid_args[:type] = method
# create Google Chart URL from given args
return create(valid_args)
else
raise "Unsupported chart type: #{method}"
end
end
# constructor
private
def initialize(args = {})
# go through arguments and set if they are applicable / valid
args.each do |key, value|
if self.respond_to? "#{key}="
unless (value.nil? || value.class.to_s == "Array" && value.empty?)
self.send "#{key}=", value
end
end
end
# call process - this will process arguments and generate the corresponding URL,
# providing everything is valid
process
end
# helper, create instance of this class, process args and return
# final Google Chart URL
def self.create(args)
# create new chart, based on supported args and process arguments
chart = Chart.new(args)
# return final Google Chart URL
return chart.url
end
# process all arguments (mapped to instance variables) and return
# final Google Chart URL
def process
# go through all instance variables
arr_processed = instance_variables.map do |variable|
# if the value of this variable is non-null
unless self.instance_variable_get(variable).nil?
# generate corresponding handler
handler = "process_#{variable.to_s.delete('@')}"
# check if handler exists and if so, execute it
if self.private_methods(false).include? handler
self.send handler
end
end
end.compact
# construct the Google Chart URL from processed arguments
@url = "http://chart.apis.google.com/chart?" + arr_processed.join("&")
end
# returns whether the specified data is one-dimensional (array) or
# multi-dimensional (array of arrays)
def multi_dimensional?(object)
if check_type(object, :array)
if object.empty?
return false
else
return check_type(object.first, :array)
end
end
return false
end
# checks the type of object
def check_type(object, object_type)
case object_type
when :string
return object.class.to_s == "String"
when :array
return object.class.to_s == "Array"
when :hash
return object.class.to_s == "Hash"
when :fixnum
return object.class.to_s == "Fixnum"
when :symbol
return object.class.to_s == "Symbol"
else
return false
end
end
# retrieves value from the hash, use supplied value if does not exist
def retrieve_value(value, default = nil)
if value.nil?
return default
end
return value
end
# process chart type
def process_type
"cht=" + case @type
when :bar
if @grouped
if @orientation == :vertical
"bvg"
elsif @orientation == :horizontal
"bhg"
else
"bvg"
end
else
if @orientation == :vertical
"bvs"
elsif @orientation == :horizontal
"bhs"
else
"bvs"
end
end
when :pie
"p"
when :pie3d
"p3"
when :line
"lc"
when :line_xy
"lxy"
end
end
# process chart size
# two possible forms:
# array form [200, 100]
# string form "700x200"
def process_size
if check_type(@size, :string)
"chs=" + @size
elsif check_type(@size, :array)
if @size.size >= 2
"chs=#{@size[0]}x#{@size[1]}"
end
end
end
# process chart title
def process_title
"chtt=" + @title
end
# process title color/fontsize
def process_title_color
unless @title_fontsize.nil?
"chts=#{@title_color},#{@title_fontsize}"
end
end
# process chart legend
def process_legend
if check_type(@legend, :array)
"chdl=" + @legend.join("|")
elsif check_type(@legend, :string)
"chdl=" + @legend.gsub(",", "|")
end
end
# process chart legend position
def process_legend_position
"chdlp=" + case @legend_position
when :left
"l"
when :right
"r"
when :top
"t"
when :bottom
"b"
end
end
# process chart colors
# two possible variants: specified as a string (one color - 'ff00ff')
# or specified as an array of strings ['ff00ff', 'ff0000']
def process_colors
if check_type(@colors, :string)
"chco=" + @colors
elsif check_type(@colors, :array)
"chco=" + @colors.join(",")
end
end
# process chart labels (pie, pie3d, google-o-meter only)
def process_labels
# pie charts have special labels
"chl=" + @labels.join("|")
end
# helper used to process axis information
# {:axis => :x, :labels => ["Jan", "July"]}, :positions => [10, 20], :range => [0, 20, 40]}
def process_axis_labels_helper(axis_hash, index)
ret_axis = nil
ret_labels = nil
ret_positions = nil
ret_range = nil
# process :axis
if axis_hash.has_key? :axis
# specified as symbol
if check_type(axis_hash[:axis], :symbol)
case axis_hash[:axis]
when :x
ret_axis = "x"
when :bottom
ret_axis = "x"
when :right
ret_axis = "r"
when :top
ret_axis = "t"
when :left
ret_axis = "y"
when :y
ret_axis = "y"
end
elsif check_type(axis_hash[:axis], :string)
ret_axis = axis_hash[:axis]
end
end
# process :labels
if axis_hash.has_key? :labels
ret_labels = "#{index}:|#{axis_hash[:labels].join('|')}"
end
# process :positions
if axis_hash.has_key? :positions
ret_positions = "#{index},#{axis_hash[:positions].join(',')}"
end
# process :range
if axis_hash.has_key? :range
ret_range = "#{index},#{axis_hash[:range].join(',')}"
end
# :axis, :labels, :positions, :range
return ret_axis, ret_labels, ret_positions, ret_range
end
# process chart labels axis
# [{:axis => :x, :labels => ["Jan", "July"]}, :positions => [10, 20]}, ...]
def process_axis_labels
if check_type(@axis_labels, :hash)
# only have one axis
# use helper to extract data
str_axis, str_labels, str_positions, str_range = process_axis_labels_helper(@axis_labels, 0)
# must have axis and labels
if str_axis.nil? || str_labels.nil?
return nil
end
# pre-process positions info
if str_positions
str_positions = "&chxp=" + str_positions
else
str_positions = ""
end
# pre-process range info
if str_range
str_range = "&chxr=" + str_range
else
str_range = ""
end
# construct substring from these arguments
"chxt=#{str_axis}&chxl=#{str_labels}#{str_positions}#{str_range}"
elsif check_type(@axis_labels, :array)
index = 0
arr_axis = []
arr_labels = []
arr_positions = []
arr_ranges = []
# for every specified axis
@axis_labels.each do |axis_data|
# use helper to extract data
str_axis, str_labels, str_positions, str_range = process_axis_labels_helper(axis_data, index)
# must have axis and labels
unless (str_axis.nil? || str_labels.nil?)
# store axis
arr_axis << str_axis
# store labels
arr_labels << str_labels
# store positions
if str_positions
arr_positions << str_positions
end
# store range
if str_range
arr_ranges << str_range
end
end
# increment index
index += 1
end
# pre-process positions info
str_positions = ""
if arr_positions.size > 0
str_positions = "&chxp=" + arr_positions.join("|")
end
# pre-process range
str_range = ""
if arr_ranges.size > 0
str_range = "&cxhr=" + arr_ranges.join("|")
end
# construct substring from these arguments
"chxt=#{arr_axis.join(',')}&chxl=#{arr_labels.join('|')}#{str_positions}#{str_range}"
end
end
# process chart data
def process_data
if @data_encoding == :simple
encoding = "s:"
else
encoding = "t:"
end
"chd=" + encoding + if multi_dimensional? @data
# data is multi dimensional
@data.collect do |row|
row.join(",")
end.join("|")
else
# data is one dimensional
@data.join(",")
end
end
# process grid data
# three ways to specify grid
# array: [20, 20]
# array: [20, 20, 1, 20], hash: {:x => 20, :y => 20, :line => 1, :blank => 20}
def process_grid
if check_type(@grid, :hash)
if (@grid.has_key? :x) && (@grid.has_key? :y)
arr_params = []
# store x / y
arr_params << @grid[:x]
arr_params << @grid[:y]
# process line segment
if @grid[:line]
arr_params << @grid[:line]
end
# process blank segment
if @grid[:blank]
arr_params << @grid[:blank]
end
"chg=" + arr_params.join(",")
end
elsif check_type(@grid, :array)
if @grid.size >= 2
# if we have more than 4 params, ignore them
"chg=" + @grid[0, 4].join(",")
end
end
end
# process zero line
# single value: 5
# array form: [1, 2, ..]
def process_zero_line
if check_type(@zero_line, :array)
"chp=" + @zero_line.join(",")
else
"chp=#{@zero_line}"
end
end
# process bar width and spacing
# single number: 20
# array form: [20, 5, 6]
# hash form: {:width => 20, :bars => 5, :groups => 10}
def process_width_spacing
if check_type(@width_spacing, :hash)
if @width_spacing.has_key? :width
arr_params = []
# process width
arr_params << @width_spacing[:width]
# process bars
if @width_spacing.has_key? :bars
arr_params << @width_spacing[:bars]
end
# process groups
if @width_spacing.has_key? :groups
arr_params << @width_spacing[:groups]
end
"chbh=" + arr_params.join(",")
end
elsif check_type(@width_spacing, :array)
"chbh=" + @width_spacing[0, 3].join(",")
else
"chbh=#{@width_spacing}"
end
end
# helper used to process markers
def process_markers_helper(marker_hash)
# type, set, points are mandatory
if ((marker_hash.has_key? :type) && (marker_hash.has_key? :points) && (marker_hash.has_key? :set))
# find what type of marker it is
str_type = case marker_hash[:type]
when :plus
"c"
when :diamond
"d"
when :arrow
"a"
when :circle
"o"
when :square
"s"
when :projection
"v"
when :vertical_line
"V"
when :horizontal_line
"h"
when :cross
"x"
when :vertical_range
"R"
when :horizontal_range
"r"
else
"o"
end
# process color
#str_color = retrieve_value(marker_hash[:color], "000000")
str_color = retrieve_value(marker_hash[:color], nil)
# no color supplied
if str_color.nil?
# check if color has been supplied
unless (@colors.nil? && @colors[marker_hash[:set]].nil?)
# color is present - use that
str_color = @colors[marker_hash[:set]]
else
# use default black
str_color = "000000"
end
end
# process set
str_set = marker_hash[:set]
# process size
str_size = retrieve_value(marker_hash[:size], "5.0")
# process priority
#str_priority = retrieve_value(marker_hash[:priority], "")
#str_priority = "0"
# process points
points = marker_hash[:points]
# generated markers
arr_markers = []
if check_type(points, :array)
points.each do |point|
arr_markers << "#{str_type},#{str_color},#{str_set},#{point},#{str_size}"
end
elsif points == :all
# for every point
if multi_dimensional? @data
# retrieve the set
arr_row = @data[str_set.to_i]
unless arr_row.nil?
arr_row.each_index do |element_index|
arr_markers << "#{str_type},#{str_color},#{str_set},#{element_index},#{str_size}"
end
end
else
@data.each_index do |element_index|
arr_markers << "#{str_type},#{str_color},0,#{element_index},#{str_size}"
end
end
else
arr_markers << "#{str_type},#{str_color},#{str_set},#{points},#{str_size}"
end
# return produced markers
return arr_markers
end
end
# process markers
# array form: [{...}, {....}]
# hash form: {:type => :plus, :color => 'FF00FF', :set => 1,
# :points => 1 or [1,2,3] or :all, :size => 20, :priority => -1, 0, 1}
# types: plus, diamond, arrow, circle, square, projection
# vertical_line, horizontal_line, cross, vertical_range, horizontal_range
def process_markers
if check_type(@markers, :hash)
arr_markers = process_markers_helper(@markers)
if arr_markers
"chm=" + arr_markers.join("|")
end
elsif check_type(@markers, :array)
arr_markers = []
@markers.each do |marker|
arr_row = process_markers_helper(marker)
unless arr_row
return nil
end
arr_markers += arr_row
end
"chm=" + arr_markers.join("|")
end
end
end
end

View file

@ -0,0 +1,7 @@
require 'googlechart/chart'
def chart_bar(hash = {})
# generate final image
GoogleChartLib::Chart.bar hash
end

View file

@ -0,0 +1,158 @@
require 'googlechart/chart'
def chart_xy(hash = {})
#need to re-process several arguments
# labels, data, markers
param_data = hash[:data]
param_labels = hash[:labels]
# compute vertical max / min
y_min = 0
y_max = 0
param_data.each do |data|
data.each do |element|
val = element.to_i
y_min = val if y_min > val
y_max = val if y_max < val
end
end
# store original range
y_min_orig = y_min
y_max_orig = y_max
#[-200, 565] -> [0, 765] -> [0*100, 765*100] -> [0/765, 76500/765] -> [0, 100]
y_max = y_max - y_min
y_shift = y_min
y_shift *= -1 if y_shift < 0
y_min = 0
# compute horizontal spacing
x_start = 100 / (1 + param_labels.size)
# reset passed data, we need to recompute it
hash[:data] = []
# recompute data
param_data.each do |data|
# horizontal data
arr_data = []
param_labels.size.times do |iter|
arr_data << (x_start * (iter + 1)).to_s
end
# store
hash[:data] << arr_data
# vertical data
arr_data = []
data.each do |element|
if y_max > 0
arr_data << ((element + y_shift) * 100 / y_max).to_s
else
arr_data << "0"
end
end
# store
hash[:data] << arr_data
end
# process labels - horizontal
arr_labels_horiz = hash[:labels]
# compute horizontal label positions
arr_positions = []
param_labels.size.times do |iter|
arr_positions << (x_start * (iter + 1)).to_s
end
# process grid
if y_max_orig - y_min_orig == 0
# set grid
hash[:grid] = [x_start, 100]
hash[:axis_labels] = [
{:axis => :y, :labels => ['0']},
{:axis => :x, :labels => hash[:labels], :positions => arr_positions}
]
else
# set grid
hash[:grid] = [x_start, 20]
# compute vertical info
arr_labels_vert = []
step = (y_max_orig - y_min_orig) / 5.0
6.times do |iter|
arr_labels_vert << (y_min_orig + iter * step).to_s
end
#y_max_orig_extra_space = y_min_orig + 6 * step
# compute horizontal info
arr_positions = []
param_labels.size.times do |iter|
arr_positions << (x_start * (iter + 1)).to_s
end
hash[:axis_labels] = [
{:axis => :y, :labels => arr_labels_vert, :range => [y_min_orig, y_max_orig]},
{:axis => :x, :labels => hash[:labels], :positions => arr_positions}
]
end
# process markers
# reset existing markers
hash[:markers] = []
# generate circle markers for every point of every line
param_data.size.times do |iter|
hash[:markers] << {:type => :circle, :size => 5.0, :points => :all, :set => iter}
end
# generate final image
GoogleChartLib::Chart.line_xy hash
end

View file

@ -0,0 +1,82 @@
require 'rubygems'
require 'gchart'
require 'version/version'
module Version
# Represents a version branch (e.g. 1.4.x, 1.5.x)
class Branch
attr_accessor :name, :game_count, :versions
def initialize
@name = nil
@game_count = 0
@versions = []
end
# Index subversions in decreasing order
def index_versions_by_subversion
# index versions in decreasing order
@versions.sort! do |left, right|
# extract sub-version values
left_scan = left.name.scan(/\d+\.\d+\.(\d+)/)
right_scan = right.name.scan(/\d+\.\d+\.(\d+)/)
if left_scan.size == 0 || right_scan.size == 0
# if we can't extract - use string comparison
left.name <=> right.name
else
left_scan.to_s.to_i <=> right_scan.to_s.to_i
end
end
end
# Create branch chart
def create_branch_chart
arr_data = []
arr_labels = []
# Go through versions, grab game counts for each
@versions.each do |version|
# store count
arr_data << version.game_count
# no need to check for 0
percentage = sprintf("%.2f", (version.game_count.to_f / game_count.to_f) * 100.to_f)
# proper pluralization
if 1 == version.game_count
pluralization = "game"
else
pluralization = "games"
end
label = "#{version.name} (#{percentage}% , #{version.game_count} #{pluralization})"
arr_labels << label
end
# Construct pie chart for game counts
Gchart.pie(
:data => arr_data,
:size => "600x200",
:labels => arr_labels,
:axis_with_labels => 'x,y'
)
end
end
end

View file

@ -0,0 +1,60 @@
require 'rubygems'
require 'gchart'
require 'version/difficulty'
module Version
# Represents a campaign with additional info (game count by difficulty)
class Campaign
attr_accessor :game_count, :difficulties, :name
def initialize
@game_count = 0
@difficulties = []
@name = nil
end
# create difficulties chart
def create_difficulty_chart
arr_data = []
arr_labels = []
# iterate through each difficulty present
difficulties.each do |difficulty|
if difficulty.game_count > 0
arr_data << difficulty.game_count
percentage = sprintf("%.2f", (difficulty.game_count.to_f / game_count.to_f) * 100.to_f)
# proper pluralization
if 1 == difficulty.game_count
pluralization = "game"
else
pluralization = "games"
end
label = "#{difficulty.name} (#{percentage}% , #{difficulty.game_count} #{pluralization})"
arr_labels << label
end
end
# Construct pie chart for game counts
Gchart.pie(
:data => arr_data,
:size => "600x200",
:labels => arr_labels,
:axis_with_labels => 'x,y'
)
end
end
end

View file

@ -0,0 +1,14 @@
module Version
# Represents a difficulty (game counts, name)
class Difficulty
attr_accessor :game_count, :name
def initialize
@name = nil
@game_count = 0
end
end
end

View file

@ -0,0 +1,18 @@
require 'version/campaign'
module Version
# Represents a version with additional info (game count, dates)
class Version
attr_accessor :game_count, :name, :campaigns
def initialize
@name = nil
@game_count = 0
@campaigns = []
end
end
end

View file

@ -0,0 +1,41 @@
# 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
# If your Rails application is accessed via an Alias directive,
# then you MUST also set the RewriteBase in this htaccess file.
#
# Example:
# Alias /myrailsapp /path/to/myrailsapp/public
# RewriteBase /myrailsapp
RewriteRule ^$ index.html [QSA]
RewriteRule ^([^.]+)$ $1.html [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
#NEVER USE dispatch.cgi!
# 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"

View file

@ -0,0 +1,30 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>The page you were looking for doesn't exist (404)</title>
<style type="text/css">
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
div.dialog {
width: 25em;
padding: 0 4em;
margin: 4em auto 0 auto;
border: 1px solid #ccc;
border-right-color: #999;
border-bottom-color: #999;
}
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
</style>
</head>
<body>
<!-- This file lives in public/404.html -->
<div class="dialog">
<h1>The page you were looking for doesn't exist.</h1>
<p>You may have mistyped the address or the page may have moved.</p>
</div>
</body>
</html>

View file

@ -0,0 +1,30 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>The change you wanted was rejected (422)</title>
<style type="text/css">
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
div.dialog {
width: 25em;
padding: 0 4em;
margin: 4em auto 0 auto;
border: 1px solid #ccc;
border-right-color: #999;
border-bottom-color: #999;
}
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
</style>
</head>
<body>
<!-- This file lives in public/422.html -->
<div class="dialog">
<h1>The change you wanted was rejected.</h1>
<p>Maybe you tried to change something you didn't have access to.</p>
</div>
</body>
</html>

View file

@ -0,0 +1,30 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>We're sorry, but something went wrong (500)</title>
<style type="text/css">
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
div.dialog {
width: 25em;
padding: 0 4em;
margin: 4em auto 0 auto;
border: 1px solid #ccc;
border-right-color: #999;
border-bottom-color: #999;
}
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
</style>
</head>
<body>
<!-- This file lives in public/500.html -->
<div class="dialog">
<h1>We're sorry, but something went wrong.</h1>
<p>We've been notified about this issue and we'll take a look at it shortly.</p>
</div>
</body>
</html>

View file

@ -0,0 +1,10 @@
#!/usr/local/bin/ruby
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

View file

@ -0,0 +1,24 @@
#!/usr/local/bin/ruby
#
# 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!

View file

@ -0,0 +1,10 @@
#!/usr/local/bin/ruby
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

View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,277 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>Ruby on Rails: Welcome aboard</title>
<style type="text/css" media="screen">
body {
margin: 0;
margin-bottom: 25px;
padding: 0;
background-color: #f0f0f0;
font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
font-size: 13px;
color: #333;
}
h1 {
font-size: 28px;
color: #000;
}
a {color: #03c}
a:hover {
background-color: #03c;
color: white;
text-decoration: none;
}
#page {
background-color: #f0f0f0;
width: 750px;
margin: 0;
margin-left: auto;
margin-right: auto;
}
#content {
float: left;
background-color: white;
border: 3px solid #aaa;
border-top: none;
padding: 25px;
width: 500px;
}
#sidebar {
float: right;
width: 175px;
}
#footer {
clear: both;
}
#header, #about, #getting-started {
padding-left: 75px;
padding-right: 30px;
}
#header {
background-image: url("images/rails.png");
background-repeat: no-repeat;
background-position: top left;
height: 64px;
}
#header h1, #header h2 {margin: 0}
#header h2 {
color: #888;
font-weight: normal;
font-size: 16px;
}
#about h3 {
margin: 0;
margin-bottom: 10px;
font-size: 14px;
}
#about-content {
background-color: #ffd;
border: 1px solid #fc0;
margin-left: -11px;
}
#about-content table {
margin-top: 10px;
margin-bottom: 10px;
font-size: 11px;
border-collapse: collapse;
}
#about-content td {
padding: 10px;
padding-top: 3px;
padding-bottom: 3px;
}
#about-content td.name {color: #555}
#about-content td.value {color: #000}
#about-content.failure {
background-color: #fcc;
border: 1px solid #f00;
}
#about-content.failure p {
margin: 0;
padding: 10px;
}
#getting-started {
border-top: 1px solid #ccc;
margin-top: 25px;
padding-top: 15px;
}
#getting-started h1 {
margin: 0;
font-size: 20px;
}
#getting-started h2 {
margin: 0;
font-size: 14px;
font-weight: normal;
color: #333;
margin-bottom: 25px;
}
#getting-started ol {
margin-left: 0;
padding-left: 0;
}
#getting-started li {
font-size: 18px;
color: #888;
margin-bottom: 25px;
}
#getting-started li h2 {
margin: 0;
font-weight: normal;
font-size: 18px;
color: #333;
}
#getting-started li p {
color: #555;
font-size: 13px;
}
#search {
margin: 0;
padding-top: 10px;
padding-bottom: 10px;
font-size: 11px;
}
#search input {
font-size: 11px;
margin: 2px;
}
#search-text {width: 170px}
#sidebar ul {
margin-left: 0;
padding-left: 0;
}
#sidebar ul h3 {
margin-top: 25px;
font-size: 16px;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
}
#sidebar li {
list-style-type: none;
}
#sidebar ul.links li {
margin-bottom: 5px;
}
</style>
<script type="text/javascript" src="javascripts/prototype.js"></script>
<script type="text/javascript" src="javascripts/effects.js"></script>
<script type="text/javascript">
function about() {
if (Element.empty('about-content')) {
new Ajax.Updater('about-content', 'rails/info/properties', {
method: 'get',
onFailure: function() {Element.classNames('about-content').add('failure')},
onComplete: function() {new Effect.BlindDown('about-content', {duration: 0.25})}
});
} else {
new Effect[Element.visible('about-content') ?
'BlindUp' : 'BlindDown']('about-content', {duration: 0.25});
}
}
window.onload = function() {
$('search-text').value = '';
$('search').onsubmit = function() {
$('search-text').value = 'site:rubyonrails.org ' + $F('search-text');
}
}
</script>
</head>
<body>
<div id="page">
<div id="sidebar">
<ul id="sidebar-items">
<li>
<form id="search" action="http://www.google.com/search" method="get">
<input type="hidden" name="hl" value="en" />
<input type="text" id="search-text" name="q" value="site:rubyonrails.org " />
<input type="submit" value="Search" /> the Rails site
</form>
</li>
<li>
<h3>Join the community</h3>
<ul class="links">
<li><a href="http://www.rubyonrails.org/">Ruby on Rails</a></li>
<li><a href="http://weblog.rubyonrails.org/">Official weblog</a></li>
<li><a href="http://lists.rubyonrails.org/">Mailing lists</a></li>
<li><a href="http://wiki.rubyonrails.org/rails/pages/IRC">IRC channel</a></li>
<li><a href="http://wiki.rubyonrails.org/">Wiki</a></li>
<li><a href="http://dev.rubyonrails.org/">Bug tracker</a></li>
</ul>
</li>
<li>
<h3>Browse the documentation</h3>
<ul class="links">
<li><a href="http://api.rubyonrails.org/">Rails API</a></li>
<li><a href="http://stdlib.rubyonrails.org/">Ruby standard library</a></li>
<li><a href="http://corelib.rubyonrails.org/">Ruby core</a></li>
</ul>
</li>
</ul>
</div>
<div id="content">
<div id="header">
<h1>Welcome aboard</h1>
<h2>You&rsquo;re riding Ruby on Rails!</h2>
</div>
<div id="about">
<h3><a href="rails/info/properties" onclick="about(); return false">About your application&rsquo;s environment</a></h3>
<div id="about-content" style="display: none"></div>
</div>
<div id="getting-started">
<h1>Getting started</h1>
<h2>Here&rsquo;s how to get rolling:</h2>
<ol>
<li>
<h2>Create your databases and edit <tt>config/database.yml</tt></h2>
<p>Rails needs to know your login and password.</p>
</li>
<li>
<h2>Use <tt>script/generate</tt> to create your models and controllers</h2>
<p>To see all available options, run it without parameters.</p>
</li>
<li>
<h2>Set up a default route and remove or rename this file</h2>
<p>Routes are set up in config/routes.rb.</p>
</li>
</ol>
</div>
</div>
<div id="footer">&nbsp;</div>
</div>
</body>
</html>

View file

@ -0,0 +1,2 @@
// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults

View file

@ -0,0 +1,963 @@
// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// Rob Wills
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
// 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.getToken(), 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: [',', '\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.
if(typeof Effect == 'undefined')
throw("controls.js requires including script.aculo.us' effects.js library");
var Autocompleter = { }
Autocompleter.Base = Class.create({
baseInitialize: function(element, update, options) {
element = $(element)
this.element = element;
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
this.oldElementValue = this.element.value;
if(this.setOptions)
this.setOptions(options);
else
this.options = options || { };
this.options.paramName = this.options.paramName || this.element.name;
this.options.tokens = this.options.tokens || [];
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {
setHeight: false,
offsetTop: element.offsetHeight
});
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if(typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
// Force carriage returns as token delimiters anyway
if (!this.options.tokens.include('\n'))
this.options.tokens.push('\n');
this.observer = null;
this.element.setAttribute('autocomplete','off');
Element.hide(this.update);
Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
},
show: function() {
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
if(!this.iefix &&
(Prototype.Browser.IE) &&
(Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
}
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
},
fixIEOverlapping: function() {
Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
Element.show(this.iefix);
},
hide: function() {
this.stopIndicator();
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
if(this.iefix) Element.hide(this.iefix);
},
startIndicator: function() {
if(this.options.indicator) Element.show(this.options.indicator);
},
stopIndicator: function() {
if(this.options.indicator) Element.hide(this.options.indicator);
},
onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
this.selectEntry();
Event.stop(event);
case Event.KEY_ESC:
this.hide();
this.active = false;
Event.stop(event);
return;
case Event.KEY_LEFT:
case Event.KEY_RIGHT:
return;
case Event.KEY_UP:
this.markPrevious();
this.render();
Event.stop(event);
return;
case Event.KEY_DOWN:
this.markNext();
this.render();
Event.stop(event);
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
(Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
activate: function() {
this.changed = false;
this.hasFocus = true;
this.getUpdatedChoices();
},
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.selectEntry();
this.hide();
},
onBlur: function(event) {
// needed to make click events working
setTimeout(this.hide.bind(this), 250);
this.hasFocus = false;
this.active = false;
},
render: function() {
if(this.entryCount > 0) {
for (var i = 0; i < this.entryCount; i++)
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
Element.removeClassName(this.getEntry(i),"selected");
if(this.hasFocus) {
this.show();
this.active = true;
}
} else {
this.active = false;
this.hide();
}
},
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
this.getEntry(this.index).scrollIntoView(true);
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
this.getEntry(this.index).scrollIntoView(false);
},
getEntry: function(index) {
return this.update.firstChild.childNodes[index];
},
getCurrentEntry: function() {
return this.getEntry(this.index);
},
selectEntry: function() {
this.active = false;
this.updateElement(this.getCurrentEntry());
},
updateElement: function(selectedElement) {
if (this.options.updateElement) {
this.options.updateElement(selectedElement);
return;
}
var value = '';
if (this.options.select) {
var nodes = $(selectedElement).select('.' + this.options.select) || [];
if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
} else
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var bounds = this.getTokenBounds();
if (bounds[0] != -1) {
var newValue = this.element.value.substr(0, bounds[0]);
var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
this.element.value = newValue + value + this.element.value.substr(bounds[1]);
} else {
this.element.value = value;
}
this.oldElementValue = this.element.value;
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
updateChoices: function(choices) {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.down());
if(this.update.firstChild && this.update.down().childNodes) {
this.entryCount =
this.update.down().childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
if(this.entryCount==1 && this.options.autoSelect) {
this.selectEntry();
this.hide();
} else {
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;
this.tokenBounds = null;
if(this.getToken().length>=this.options.minChars) {
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
this.oldElementValue = this.element.value;
},
getToken: function() {
var bounds = this.getTokenBounds();
return this.element.value.substring(bounds[0], bounds[1]).strip();
},
getTokenBounds: function() {
if (null != this.tokenBounds) return this.tokenBounds;
var value = this.element.value;
if (value.strip().empty()) return [-1, 0];
var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
var offset = (diff == this.oldElementValue.length ? 1 : 0);
var prevTokenPos = -1, nextTokenPos = value.length;
var tp;
for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
if (tp > prevTokenPos) prevTokenPos = tp;
tp = value.indexOf(this.options.tokens[index], diff + offset);
if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
}
return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
}
});
Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
var boundary = Math.min(newS.length, oldS.length);
for (var index = 0; index < boundary; ++index)
if (newS[index] != oldS[index])
return index;
return boundary;
};
Ajax.Autocompleter = Class.create(Autocompleter.Base, {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
getUpdatedChoices: function() {
this.startIndicator();
var entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
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
//
// - partialSearch - 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 fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
// a partial match (unlike minChars, which defines
// how many characters are required to do any match
// at all). Defaults to 2.
//
// - ignoreCase - 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.Base, {
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
},
getUpdatedChoices: function() {
this.updateChoices(this.options.selector(this));
},
setOptions: function(options) {
this.options = Object.extend({
choices: 10,
partialSearch: true,
partialChars: 2,
ignoreCase: true,
fullSearch: false,
selector: function(instance) {
var ret = []; // Beginning matches
var partial = []; // Inside matches
var entry = instance.getToken();
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 foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 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.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
foundPos + entry.length) + "</li>");
break;
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
return "<ul>" + ret.join('') + "</ul>";
}
}, options || { });
}
});
// AJAX in-place editor and collection editor
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
setTimeout(function() {
Field.activate(field);
}, 1);
}
Ajax.InPlaceEditor = Class.create({
initialize: function(element, url, options) {
this.url = url;
this.element = element = $(element);
this.prepareOptions();
this._controls = { };
arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
Object.extend(this.options, options || { });
if (!this.options.formId && this.element.id) {
this.options.formId = this.element.id + '-inplaceeditor';
if ($(this.options.formId))
this.options.formId = '';
}
if (this.options.externalControl)
this.options.externalControl = $(this.options.externalControl);
if (!this.options.externalControl)
this.options.externalControlOnly = false;
this._originalBackground = this.element.getStyle('background-color') || 'transparent';
this.element.title = this.options.clickToEditText;
this._boundCancelHandler = this.handleFormCancellation.bind(this);
this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
this._boundFailureHandler = this.handleAJAXFailure.bind(this);
this._boundSubmitHandler = this.handleFormSubmission.bind(this);
this._boundWrapperHandler = this.wrapUp.bind(this);
this.registerListeners();
},
checkForEscapeOrReturn: function(e) {
if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
if (Event.KEY_ESC == e.keyCode)
this.handleFormCancellation(e);
else if (Event.KEY_RETURN == e.keyCode)
this.handleFormSubmission(e);
},
createControl: function(mode, handler, extraClasses) {
var control = this.options[mode + 'Control'];
var text = this.options[mode + 'Text'];
if ('button' == control) {
var btn = document.createElement('input');
btn.type = 'submit';
btn.value = text;
btn.className = 'editor_' + mode + '_button';
if ('cancel' == mode)
btn.onclick = this._boundCancelHandler;
this._form.appendChild(btn);
this._controls[mode] = btn;
} else if ('link' == control) {
var link = document.createElement('a');
link.href = '#';
link.appendChild(document.createTextNode(text));
link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
link.className = 'editor_' + mode + '_link';
if (extraClasses)
link.className += ' ' + extraClasses;
this._form.appendChild(link);
this._controls[mode] = link;
}
},
createEditField: function() {
var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
var fld;
if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
fld = document.createElement('input');
fld.type = 'text';
var size = this.options.size || this.options.cols || 0;
if (0 < size) fld.size = size;
} else {
fld = document.createElement('textarea');
fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
fld.cols = this.options.cols || 40;
}
fld.name = this.options.paramName;
fld.value = text; // No HTML breaks conversion anymore
fld.className = 'editor_field';
if (this.options.submitOnBlur)
fld.onblur = this._boundSubmitHandler;
this._controls.editor = fld;
if (this.options.loadTextURL)
this.loadExternalText();
this._form.appendChild(this._controls.editor);
},
createForm: function() {
var ipe = this;
function addText(mode, condition) {
var text = ipe.options['text' + mode + 'Controls'];
if (!text || condition === false) return;
ipe._form.appendChild(document.createTextNode(text));
};
this._form = $(document.createElement('form'));
this._form.id = this.options.formId;
this._form.addClassName(this.options.formClassName);
this._form.onsubmit = this._boundSubmitHandler;
this.createEditField();
if ('textarea' == this._controls.editor.tagName.toLowerCase())
this._form.appendChild(document.createElement('br'));
if (this.options.onFormCustomization)
this.options.onFormCustomization(this, this._form);
addText('Before', this.options.okControl || this.options.cancelControl);
this.createControl('ok', this._boundSubmitHandler);
addText('Between', this.options.okControl && this.options.cancelControl);
this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
addText('After', this.options.okControl || this.options.cancelControl);
},
destroy: function() {
if (this._oldInnerHTML)
this.element.innerHTML = this._oldInnerHTML;
this.leaveEditMode();
this.unregisterListeners();
},
enterEditMode: function(e) {
if (this._saving || this._editing) return;
this._editing = true;
this.triggerCallback('onEnterEditMode');
if (this.options.externalControl)
this.options.externalControl.hide();
this.element.hide();
this.createForm();
this.element.parentNode.insertBefore(this._form, this.element);
if (!this.options.loadTextURL)
this.postProcessEditField();
if (e) Event.stop(e);
},
enterHover: function(e) {
if (this.options.hoverClassName)
this.element.addClassName(this.options.hoverClassName);
if (this._saving) return;
this.triggerCallback('onEnterHover');
},
getText: function() {
return this.element.innerHTML;
},
handleAJAXFailure: function(transport) {
this.triggerCallback('onFailure', transport);
if (this._oldInnerHTML) {
this.element.innerHTML = this._oldInnerHTML;
this._oldInnerHTML = null;
}
},
handleFormCancellation: function(e) {
this.wrapUp();
if (e) Event.stop(e);
},
handleFormSubmission: function(e) {
var form = this._form;
var value = $F(this._controls.editor);
this.prepareSubmission();
var params = this.options.callback(form, value) || '';
if (Object.isString(params))
params = params.toQueryParams();
params.editorId = this.element.id;
if (this.options.htmlResponse) {
var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
Object.extend(options, {
parameters: params,
onComplete: this._boundWrapperHandler,
onFailure: this._boundFailureHandler
});
new Ajax.Updater({ success: this.element }, this.url, options);
} else {
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Object.extend(options, {
parameters: params,
onComplete: this._boundWrapperHandler,
onFailure: this._boundFailureHandler
});
new Ajax.Request(this.url, options);
}
if (e) Event.stop(e);
},
leaveEditMode: function() {
this.element.removeClassName(this.options.savingClassName);
this.removeForm();
this.leaveHover();
this.element.style.backgroundColor = this._originalBackground;
this.element.show();
if (this.options.externalControl)
this.options.externalControl.show();
this._saving = false;
this._editing = false;
this._oldInnerHTML = null;
this.triggerCallback('onLeaveEditMode');
},
leaveHover: function(e) {
if (this.options.hoverClassName)
this.element.removeClassName(this.options.hoverClassName);
if (this._saving) return;
this.triggerCallback('onLeaveHover');
},
loadExternalText: function() {
this._form.addClassName(this.options.loadingClassName);
this._controls.editor.disabled = true;
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Object.extend(options, {
parameters: 'editorId=' + encodeURIComponent(this.element.id),
onComplete: Prototype.emptyFunction,
onSuccess: function(transport) {
this._form.removeClassName(this.options.loadingClassName);
var text = transport.responseText;
if (this.options.stripLoadedTextTags)
text = text.stripTags();
this._controls.editor.value = text;
this._controls.editor.disabled = false;
this.postProcessEditField();
}.bind(this),
onFailure: this._boundFailureHandler
});
new Ajax.Request(this.options.loadTextURL, options);
},
postProcessEditField: function() {
var fpc = this.options.fieldPostCreation;
if (fpc)
$(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
},
prepareOptions: function() {
this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
[this._extraDefaultOptions].flatten().compact().each(function(defs) {
Object.extend(this.options, defs);
}.bind(this));
},
prepareSubmission: function() {
this._saving = true;
this.removeForm();
this.leaveHover();
this.showSaving();
},
registerListeners: function() {
this._listeners = { };
var listener;
$H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
listener = this[pair.value].bind(this);
this._listeners[pair.key] = listener;
if (!this.options.externalControlOnly)
this.element.observe(pair.key, listener);
if (this.options.externalControl)
this.options.externalControl.observe(pair.key, listener);
}.bind(this));
},
removeForm: function() {
if (!this._form) return;
this._form.remove();
this._form = null;
this._controls = { };
},
showSaving: function() {
this._oldInnerHTML = this.element.innerHTML;
this.element.innerHTML = this.options.savingText;
this.element.addClassName(this.options.savingClassName);
this.element.style.backgroundColor = this._originalBackground;
this.element.show();
},
triggerCallback: function(cbName, arg) {
if ('function' == typeof this.options[cbName]) {
this.options[cbName](this, arg);
}
},
unregisterListeners: function() {
$H(this._listeners).each(function(pair) {
if (!this.options.externalControlOnly)
this.element.stopObserving(pair.key, pair.value);
if (this.options.externalControl)
this.options.externalControl.stopObserving(pair.key, pair.value);
}.bind(this));
},
wrapUp: function(transport) {
this.leaveEditMode();
// Can't use triggerCallback due to backward compatibility: requires
// binding + direct element
this._boundComplete(transport, this.element);
}
});
Object.extend(Ajax.InPlaceEditor.prototype, {
dispose: Ajax.InPlaceEditor.prototype.destroy
});
Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
initialize: function($super, element, url, options) {
this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
$super(element, url, options);
},
createEditField: function() {
var list = document.createElement('select');
list.name = this.options.paramName;
list.size = 1;
this._controls.editor = list;
this._collection = this.options.collection || [];
if (this.options.loadCollectionURL)
this.loadCollection();
else
this.checkForExternalText();
this._form.appendChild(this._controls.editor);
},
loadCollection: function() {
this._form.addClassName(this.options.loadingClassName);
this.showLoadingText(this.options.loadingCollectionText);
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Object.extend(options, {
parameters: 'editorId=' + encodeURIComponent(this.element.id),
onComplete: Prototype.emptyFunction,
onSuccess: function(transport) {
var js = transport.responseText.strip();
if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
throw 'Server returned an invalid collection representation.';
this._collection = eval(js);
this.checkForExternalText();
}.bind(this),
onFailure: this.onFailure
});
new Ajax.Request(this.options.loadCollectionURL, options);
},
showLoadingText: function(text) {
this._controls.editor.disabled = true;
var tempOption = this._controls.editor.firstChild;
if (!tempOption) {
tempOption = document.createElement('option');
tempOption.value = '';
this._controls.editor.appendChild(tempOption);
tempOption.selected = true;
}
tempOption.update((text || '').stripScripts().stripTags());
},
checkForExternalText: function() {
this._text = this.getText();
if (this.options.loadTextURL)
this.loadExternalText();
else
this.buildOptionList();
},
loadExternalText: function() {
this.showLoadingText(this.options.loadingText);
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Object.extend(options, {
parameters: 'editorId=' + encodeURIComponent(this.element.id),
onComplete: Prototype.emptyFunction,
onSuccess: function(transport) {
this._text = transport.responseText.strip();
this.buildOptionList();
}.bind(this),
onFailure: this.onFailure
});
new Ajax.Request(this.options.loadTextURL, options);
},
buildOptionList: function() {
this._form.removeClassName(this.options.loadingClassName);
this._collection = this._collection.map(function(entry) {
return 2 === entry.length ? entry : [entry, entry].flatten();
});
var marker = ('value' in this.options) ? this.options.value : this._text;
var textFound = this._collection.any(function(entry) {
return entry[0] == marker;
}.bind(this));
this._controls.editor.update('');
var option;
this._collection.each(function(entry, index) {
option = document.createElement('option');
option.value = entry[0];
option.selected = textFound ? entry[0] == marker : 0 == index;
option.appendChild(document.createTextNode(entry[1]));
this._controls.editor.appendChild(option);
}.bind(this));
this._controls.editor.disabled = false;
Field.scrollFreeActivate(this._controls.editor);
}
});
//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
//**** This only exists for a while, in order to let ****
//**** users adapt to the new API. Read up on the new ****
//**** API and convert your code to it ASAP! ****
Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
if (!options) return;
function fallback(name, expr) {
if (name in options || expr === undefined) return;
options[name] = expr;
};
fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
options.cancelLink == options.cancelButton == false ? false : undefined)));
fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
options.okLink == options.okButton == false ? false : undefined)));
fallback('highlightColor', options.highlightcolor);
fallback('highlightEndColor', options.highlightendcolor);
};
Object.extend(Ajax.InPlaceEditor, {
DefaultOptions: {
ajaxOptions: { },
autoRows: 3, // Use when multi-line w/ rows == 1
cancelControl: 'link', // 'link'|'button'|false
cancelText: 'cancel',
clickToEditText: 'Click to edit',
externalControl: null, // id|elt
externalControlOnly: false,
fieldPostCreation: 'activate', // 'activate'|'focus'|false
formClassName: 'inplaceeditor-form',
formId: null, // id|elt
highlightColor: '#ffff99',
highlightEndColor: '#ffffff',
hoverClassName: '',
htmlResponse: true,
loadingClassName: 'inplaceeditor-loading',
loadingText: 'Loading...',
okControl: 'button', // 'link'|'button'|false
okText: 'ok',
paramName: 'value',
rows: 1, // If 1 and multi-line, uses autoRows
savingClassName: 'inplaceeditor-saving',
savingText: 'Saving...',
size: 0,
stripLoadedTextTags: false,
submitOnBlur: false,
textAfterControls: '',
textBeforeControls: '',
textBetweenControls: ''
},
DefaultCallbacks: {
callback: function(form) {
return Form.serialize(form);
},
onComplete: function(transport, element) {
// For backward compatibility, this one is bound to the IPE, and passes
// the element directly. It was too often customized, so we don't break it.
new Effect.Highlight(element, {
startcolor: this.options.highlightColor, keepBackgroundImage: true });
},
onEnterEditMode: null,
onEnterHover: function(ipe) {
ipe.element.style.backgroundColor = ipe.options.highlightColor;
if (ipe._effect)
ipe._effect.cancel();
},
onFailure: function(transport, ipe) {
alert('Error communication with the server: ' + transport.responseText.stripTags());
},
onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
onLeaveEditMode: null,
onLeaveHover: function(ipe) {
ipe._effect = new Effect.Highlight(ipe.element, {
startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
restorecolor: ipe._originalBackground, keepBackgroundImage: true
});
}
},
Listeners: {
click: 'enterEditMode',
keydown: 'checkForEscapeOrReturn',
mouseover: 'enterHover',
mouseout: 'leaveHover'
}
});
Ajax.InPlaceCollectionEditor.DefaultOptions = {
loadingCollectionText: 'Loading options...'
};
// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields
Form.Element.DelayedObserver = Class.create({
initialize: function(element, delay, callback) {
this.delay = delay || 0.5;
this.element = $(element);
this.callback = callback;
this.timer = null;
this.lastValue = $F(this.element);
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
},
delayedListener: function(event) {
if(this.lastValue == $F(this.element)) return;
if(this.timer) clearTimeout(this.timer);
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
this.lastValue = $F(this.element);
},
onTimerEvent: function() {
this.timer = null;
this.callback(this.element, $F(this.element));
}
});

View file

@ -0,0 +1,972 @@
// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/
if(Object.isUndefined(Effect))
throw("dragdrop.js requires including script.aculo.us' effects.js library");
var Droppables = {
drops: [],
remove: function(element) {
this.drops = this.drops.reject(function(d) { return d.element==$(element) });
},
add: function(element) {
element = $(element);
var options = Object.extend({
greedy: true,
hoverclass: null,
tree: false
}, arguments[1] || { });
// cache containers
if(options.containment) {
options._containers = [];
var containment = options.containment;
if(Object.isArray(containment)) {
containment.each( function(c) { options._containers.push($(c)) });
} else {
options._containers.push($(containment));
}
}
if(options.accept) options.accept = [options.accept].flatten();
Element.makePositioned(element); // fix IE
options.element = element;
this.drops.push(options);
},
findDeepestChild: function(drops) {
deepest = drops[0];
for (i = 1; i < drops.length; ++i)
if (Element.isParent(drops[i].element, deepest.element))
deepest = drops[i];
return deepest;
},
isContained: function(element, drop) {
var containmentNode;
if(drop.tree) {
containmentNode = element.treeNode;
} else {
containmentNode = element.parentNode;
}
return drop._containers.detect(function(c) { return containmentNode == c });
},
isAffected: function(point, element, drop) {
return (
(drop.element!=element) &&
((!drop._containers) ||
this.isContained(element, drop)) &&
((!drop.accept) ||
(Element.classNames(element).detect(
function(v) { return drop.accept.include(v) } ) )) &&
Position.within(drop.element, point[0], point[1]) );
},
deactivate: function(drop) {
if(drop.hoverclass)
Element.removeClassName(drop.element, drop.hoverclass);
this.last_active = null;
},
activate: function(drop) {
if(drop.hoverclass)
Element.addClassName(drop.element, drop.hoverclass);
this.last_active = drop;
},
show: function(point, element) {
if(!this.drops.length) return;
var drop, affected = [];
this.drops.each( function(drop) {
if(Droppables.isAffected(point, element, drop))
affected.push(drop);
});
if(affected.length>0)
drop = Droppables.findDeepestChild(affected);
if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
if (drop) {
Position.within(drop.element, point[0], point[1]);
if(drop.onHover)
drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
if (drop != this.last_active) Droppables.activate(drop);
}
},
fire: function(event, element) {
if(!this.last_active) return;
Position.prepare();
if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
if (this.last_active.onDrop) {
this.last_active.onDrop(element, this.last_active.element, event);
return true;
}
},
reset: function() {
if(this.last_active)
this.deactivate(this.last_active);
}
}
var Draggables = {
drags: [],
observers: [],
register: function(draggable) {
if(this.drags.length == 0) {
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
}
this.drags.push(draggable);
},
unregister: function(draggable) {
this.drags = this.drags.reject(function(d) { return d==draggable });
if(this.drags.length == 0) {
Event.stopObserving(document, "mouseup", this.eventMouseUp);
Event.stopObserving(document, "mousemove", this.eventMouseMove);
Event.stopObserving(document, "keypress", this.eventKeypress);
}
},
activate: function(draggable) {
if(draggable.options.delay) {
this._timeout = setTimeout(function() {
Draggables._timeout = null;
window.focus();
Draggables.activeDraggable = draggable;
}.bind(this), draggable.options.delay);
} else {
window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
this.activeDraggable = draggable;
}
},
deactivate: function() {
this.activeDraggable = null;
},
updateDrag: function(event) {
if(!this.activeDraggable) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
// Mozilla-based browsers fire successive mousemove events with
// the same coordinates, prevent needless redrawing (moz bug?)
if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
this._lastPointer = pointer;
this.activeDraggable.updateDrag(event, pointer);
},
endDrag: function(event) {
if(this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
if(!this.activeDraggable) return;
this._lastPointer = null;
this.activeDraggable.endDrag(event);
this.activeDraggable = null;
},
keyPress: function(event) {
if(this.activeDraggable)
this.activeDraggable.keyPress(event);
},
addObserver: function(observer) {
this.observers.push(observer);
this._cacheObserverCallbacks();
},
removeObserver: function(element) { // element instead of observer fixes mem leaks
this.observers = this.observers.reject( function(o) { return o.element==element });
this._cacheObserverCallbacks();
},
notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
if(this[eventName+'Count'] > 0)
this.observers.each( function(o) {
if(o[eventName]) o[eventName](eventName, draggable, event);
});
if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
},
_cacheObserverCallbacks: function() {
['onStart','onEnd','onDrag'].each( function(eventName) {
Draggables[eventName+'Count'] = Draggables.observers.select(
function(o) { return o[eventName]; }
).length;
});
}
}
/*--------------------------------------------------------------------------*/
var Draggable = Class.create({
initialize: function(element) {
var defaults = {
handle: false,
reverteffect: function(element, top_offset, left_offset) {
var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
queue: {scope:'_draggable', position:'end'}
});
},
endeffect: function(element) {
var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
queue: {scope:'_draggable', position:'end'},
afterFinish: function(){
Draggable._dragging[element] = false
}
});
},
zindex: 1000,
revert: false,
quiet: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
delay: 0
};
if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
Object.extend(defaults, {
starteffect: function(element) {
element._opacity = Element.getOpacity(element);
Draggable._dragging[element] = true;
new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
}
});
var options = Object.extend(defaults, arguments[1] || { });
this.element = $(element);
if(options.handle && Object.isString(options.handle))
this.handle = this.element.down('.'+options.handle, 0);
if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element;
if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
options.scroll = $(options.scroll);
this._isScrollChild = Element.childOf(this.element, options.scroll);
}
Element.makePositioned(this.element); // fix IE
this.options = options;
this.dragging = false;
this.eventMouseDown = this.initDrag.bindAsEventListener(this);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
Draggables.register(this);
},
destroy: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
Draggables.unregister(this);
},
currentDelta: function() {
return([
parseInt(Element.getStyle(this.element,'left') || '0'),
parseInt(Element.getStyle(this.element,'top') || '0')]);
},
initDrag: function(event) {
if(!Object.isUndefined(Draggable._dragging[this.element]) &&
Draggable._dragging[this.element]) return;
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
if((tag_name = src.tagName.toUpperCase()) && (
tag_name=='INPUT' ||
tag_name=='SELECT' ||
tag_name=='OPTION' ||
tag_name=='BUTTON' ||
tag_name=='TEXTAREA')) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var pos = Position.cumulativeOffset(this.element);
this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
Draggables.activate(this);
Event.stop(event);
}
},
startDrag: function(event) {
this.dragging = true;
if(!this.delta)
this.delta = this.currentDelta();
if(this.options.zindex) {
this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
this.element.style.zIndex = this.options.zindex;
}
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
if (!this.element._originallyAbsolute)
Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
if(this.options.scroll) {
if (this.options.scroll == window) {
var where = this._getWindowScroll(this.options.scroll);
this.originalScrollLeft = where.left;
this.originalScrollTop = where.top;
} else {
this.originalScrollLeft = this.options.scroll.scrollLeft;
this.originalScrollTop = this.options.scroll.scrollTop;
}
}
Draggables.notify('onStart', this, event);
if(this.options.starteffect) this.options.starteffect(this.element);
},
updateDrag: function(event, pointer) {
if(!this.dragging) this.startDrag(event);
if(!this.options.quiet){
Position.prepare();
Droppables.show(pointer, this.element);
}
Draggables.notify('onDrag', this, event);
this.draw(pointer);
if(this.options.change) this.options.change(this);
if(this.options.scroll) {
this.stopScrolling();
var p;
if (this.options.scroll == window) {
with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
} else {
p = Position.page(this.options.scroll);
p[0] += this.options.scroll.scrollLeft + Position.deltaX;
p[1] += this.options.scroll.scrollTop + Position.deltaY;
p.push(p[0]+this.options.scroll.offsetWidth);
p.push(p[1]+this.options.scroll.offsetHeight);
}
var speed = [0,0];
if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
this.startScrolling(speed);
}
// fix AppleWebKit rendering
if(Prototype.Browser.WebKit) window.scrollBy(0,0);
Event.stop(event);
},
finishDrag: function(event, success) {
this.dragging = false;
if(this.options.quiet){
Position.prepare();
var pointer = [Event.pointerX(event), Event.pointerY(event)];
Droppables.show(pointer, this.element);
}
if(this.options.ghosting) {
if (!this.element._originallyAbsolute)
Position.relativize(this.element);
delete this.element._originallyAbsolute;
Element.remove(this._clone);
this._clone = null;
}
var dropped = false;
if(success) {
dropped = Droppables.fire(event, this.element);
if (!dropped) dropped = false;
}
if(dropped && this.options.onDropped) this.options.onDropped(this.element);
Draggables.notify('onEnd', this, event);
var revert = this.options.revert;
if(revert && Object.isFunction(revert)) revert = revert(this.element);
var d = this.currentDelta();
if(revert && this.options.reverteffect) {
if (dropped == 0 || revert != 'failure')
this.options.reverteffect(this.element,
d[1]-this.delta[1], d[0]-this.delta[0]);
} else {
this.delta = d;
}
if(this.options.zindex)
this.element.style.zIndex = this.originalZ;
if(this.options.endeffect)
this.options.endeffect(this.element);
Draggables.deactivate(this);
Droppables.reset();
},
keyPress: function(event) {
if(event.keyCode!=Event.KEY_ESC) return;
this.finishDrag(event, false);
Event.stop(event);
},
endDrag: function(event) {
if(!this.dragging) return;
this.stopScrolling();
this.finishDrag(event, true);
Event.stop(event);
},
draw: function(point) {
var pos = Position.cumulativeOffset(this.element);
if(this.options.ghosting) {
var r = Position.realOffset(this.element);
pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
}
var d = this.currentDelta();
pos[0] -= d[0]; pos[1] -= d[1];
if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
}
var p = [0,1].map(function(i){
return (point[i]-pos[i]-this.offset[i])
}.bind(this));
if(this.options.snap) {
if(Object.isFunction(this.options.snap)) {
p = this.options.snap(p[0],p[1],this);
} else {
if(Object.isArray(this.options.snap)) {
p = p.map( function(v, i) {
return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this))
} else {
p = p.map( function(v) {
return (v/this.options.snap).round()*this.options.snap }.bind(this))
}
}}
var style = this.element.style;
if((!this.options.constraint) || (this.options.constraint=='horizontal'))
style.left = p[0] + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = p[1] + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
},
stopScrolling: function() {
if(this.scrollInterval) {
clearInterval(this.scrollInterval);
this.scrollInterval = null;
Draggables._lastScrollPointer = null;
}
},
startScrolling: function(speed) {
if(!(speed[0] || speed[1])) return;
this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
this.lastScrolled = new Date();
this.scrollInterval = setInterval(this.scroll.bind(this), 10);
},
scroll: function() {
var current = new Date();
var delta = current - this.lastScrolled;
this.lastScrolled = current;
if(this.options.scroll == window) {
with (this._getWindowScroll(this.options.scroll)) {
if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
var d = delta / 1000;
this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
}
}
} else {
this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
}
Position.prepare();
Droppables.show(Draggables._lastPointer, this.element);
Draggables.notify('onDrag', this);
if (this._isScrollChild) {
Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
if (Draggables._lastScrollPointer[0] < 0)
Draggables._lastScrollPointer[0] = 0;
if (Draggables._lastScrollPointer[1] < 0)
Draggables._lastScrollPointer[1] = 0;
this.draw(Draggables._lastScrollPointer);
}
if(this.options.change) this.options.change(this);
},
_getWindowScroll: function(w) {
var T, L, W, H;
with (w.document) {
if (w.document.documentElement && documentElement.scrollTop) {
T = documentElement.scrollTop;
L = documentElement.scrollLeft;
} else if (w.document.body) {
T = body.scrollTop;
L = body.scrollLeft;
}
if (w.innerWidth) {
W = w.innerWidth;
H = w.innerHeight;
} else if (w.document.documentElement && documentElement.clientWidth) {
W = documentElement.clientWidth;
H = documentElement.clientHeight;
} else {
W = body.offsetWidth;
H = body.offsetHeight
}
}
return { top: T, left: L, width: W, height: H };
}
});
Draggable._dragging = { };
/*--------------------------------------------------------------------------*/
var SortableObserver = Class.create({
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() {
Sortable.unmark();
if(this.lastValue != Sortable.serialize(this.element))
this.observer(this.element)
}
});
var Sortable = {
SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
sortables: { },
_findRootElement: function(element) {
while (element.tagName.toUpperCase() != "BODY") {
if(element.id && Sortable.sortables[element.id]) return element;
element = element.parentNode;
}
},
options: function(element) {
element = Sortable._findRootElement($(element));
if(!element) return;
return Sortable.sortables[element.id];
},
destroy: function(element){
var s = Sortable.options(element);
if(s) {
Draggables.removeObserver(s.element);
s.droppables.each(function(d){ Droppables.remove(d) });
s.draggables.invoke('destroy');
delete Sortable.sortables[s.element.id];
}
},
create: function(element) {
element = $(element);
var options = Object.extend({
element: element,
tag: 'li', // assumes li children, override with tag: 'tagname'
dropOnEmpty: false,
tree: false,
treeTag: 'ul',
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,
delay: 0,
hoverclass: null,
ghosting: false,
quiet: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
format: this.SERIALIZE_RULE,
// these take arrays of elements or ids and can be
// used for better initialization performance
elements: false,
handles: false,
onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction
}, arguments[1] || { });
// clear any old sortable with same element
this.destroy(element);
// build options for the draggables
var options_for_draggable = {
revert: true,
quiet: options.quiet,
scroll: options.scroll,
scrollSpeed: options.scrollSpeed,
scrollSensitivity: options.scrollSensitivity,
delay: options.delay,
ghosting: options.ghosting,
constraint: options.constraint,
handle: options.handle };
if(options.starteffect)
options_for_draggable.starteffect = options.starteffect;
if(options.reverteffect)
options_for_draggable.reverteffect = options.reverteffect;
else
if(options.ghosting) options_for_draggable.reverteffect = function(element) {
element.style.top = 0;
element.style.left = 0;
};
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,
tree: options.tree,
hoverclass: options.hoverclass,
onHover: Sortable.onHover
}
var options_for_tree = {
onHover: Sortable.onEmptyHover,
overlap: options.overlap,
containment: options.containment,
hoverclass: options.hoverclass
}
// fix for gecko engine
Element.cleanWhitespace(element);
options.draggables = [];
options.droppables = [];
// drop on empty handling
if(options.dropOnEmpty || options.tree) {
Droppables.add(element, options_for_tree);
options.droppables.push(element);
}
(options.elements || this.findElements(element, options) || []).each( function(e,i) {
var handle = options.handles ? $(options.handles[i]) :
(options.handle ? $(e).select('.' + options.handle)[0] : e);
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
if(options.tree) e.treeNode = element;
options.droppables.push(e);
});
if(options.tree) {
(Sortable.findTreeElements(element, options) || []).each( function(e) {
Droppables.add(e, options_for_tree);
e.treeNode = element;
options.droppables.push(e);
});
}
// keep reference
this.sortables[element.id] = options;
// for onupdate
Draggables.addObserver(new SortableObserver(element, options.onUpdate));
},
// return all suitable-for-sortable elements in a guaranteed order
findElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.tag);
},
findTreeElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.treeTag);
},
onHover: function(element, dropon, overlap) {
if(Element.isParent(dropon, element)) return;
if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
return;
} else if(overlap>0.5) {
Sortable.mark(dropon, 'before');
if(dropon.previousSibling != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, dropon);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
} else {
Sortable.mark(dropon, 'after');
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)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},
onEmptyHover: function(element, dropon, overlap) {
var oldParentNode = element.parentNode;
var droponOptions = Sortable.options(dropon);
if(!Element.isParent(dropon, element)) {
var index;
var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
var child = null;
if(children) {
var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
for (index = 0; index < children.length; index += 1) {
if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
offset -= Element.offsetSize (children[index], droponOptions.overlap);
} else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
child = index + 1 < children.length ? children[index + 1] : null;
break;
} else {
child = children[index];
break;
}
}
}
dropon.insertBefore(element, child);
Sortable.options(oldParentNode).onChange(element);
droponOptions.onChange(element);
}
},
unmark: function() {
if(Sortable._marker) Sortable._marker.hide();
},
mark: function(dropon, position) {
// mark on ghosting only
var sortable = Sortable.options(dropon.parentNode);
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker =
($('dropmarker') || Element.extend(document.createElement('DIV'))).
hide().addClassName('dropmarker').setStyle({position:'absolute'});
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
if(position=='after')
if(sortable.overlap == 'horizontal')
Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
else
Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
Sortable._marker.show();
},
_tree: function(element, options, parent) {
var children = Sortable.findElements(element, options) || [];
for (var i = 0; i < children.length; ++i) {
var match = children[i].id.match(options.format);
if (!match) continue;
var child = {
id: encodeURIComponent(match ? match[1] : null),
element: element,
parent: parent,
children: [],
position: parent.children.length,
container: $(children[i]).down(options.treeTag)
}
/* Get the element containing the children and recurse over it */
if (child.container)
this._tree(child.container, options, child)
parent.children.push (child);
}
return parent;
},
tree: function(element) {
element = $(element);
var sortableOptions = this.options(element);
var options = Object.extend({
tag: sortableOptions.tag,
treeTag: sortableOptions.treeTag,
only: sortableOptions.only,
name: element.id,
format: sortableOptions.format
}, arguments[1] || { });
var root = {
id: null,
parent: null,
children: [],
container: element,
position: 0
}
return Sortable._tree(element, options, root);
},
/* Construct a [i] index for a particular node */
_constructIndex: function(node) {
var index = '';
do {
if (node.id) index = '[' + node.position + ']' + index;
} while ((node = node.parent) != null);
return index;
},
sequence: function(element) {
element = $(element);
var options = Object.extend(this.options(element), arguments[1] || { });
return $(this.findElements(element, options) || []).map( function(item) {
return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
});
},
setSequence: function(element, new_sequence) {
element = $(element);
var options = Object.extend(this.options(element), arguments[2] || { });
var nodeMap = { };
this.findElements(element, options).each( function(n) {
if (n.id.match(options.format))
nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
n.parentNode.removeChild(n);
});
new_sequence.each(function(ident) {
var n = nodeMap[ident];
if (n) {
n[1].appendChild(n[0]);
delete nodeMap[ident];
}
});
},
serialize: function(element) {
element = $(element);
var options = Object.extend(Sortable.options(element), arguments[1] || { });
var name = encodeURIComponent(
(arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
if (options.tree) {
return Sortable.tree(element, arguments[1]).children.map( function (item) {
return [name + Sortable._constructIndex(item) + "[id]=" +
encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
}).flatten().join('&');
} else {
return Sortable.sequence(element, arguments[1]).map( function(item) {
return name + "[]=" + encodeURIComponent(item);
}).join('&');
}
}
}
// Returns true if child is contained within element
Element.isParent = function(child, element) {
if (!child.parentNode || child == element) return false;
if (child.parentNode == element) return true;
return Element.isParent(child.parentNode, element);
}
Element.findChildren = function(element, only, recursive, tagName) {
if(!element.hasChildNodes()) return null;
tagName = tagName.toUpperCase();
if(only) only = [only].flatten();
var elements = [];
$A(element.childNodes).each( function(e) {
if(e.tagName && e.tagName.toUpperCase()==tagName &&
(!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
elements.push(e);
if(recursive) {
var grandchildren = Element.findChildren(e, only, recursive, tagName);
if(grandchildren) elements.push(grandchildren);
}
});
return (elements.length>0 ? elements.flatten() : []);
}
Element.offsetSize = function (element, type) {
return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-Agent: *
# Disallow: /

View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/about'

View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/console'

View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/destroy'

View file

@ -0,0 +1,5 @@
#!/usr/bin/env ruby
system "export GEM_HOME=$HOME/.gems"
system "export GEM_PATH=/usr/local/lib/ruby/gems/1.8:$GEM_HOME"

View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/generate'

View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/performance/benchmarker'

View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/performance/profiler'

View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/performance/request'

View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/plugin'

View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/process/inspector'

View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/process/reaper'

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