1
0
mirror of https://github.com/danbee/danbarberphoto synced 2025-03-04 08:49:07 +00:00

Initial commit.

This commit is contained in:
Dan Barber 2010-10-07 11:02:13 -04:00
commit 48cc5d8de8
130 changed files with 13202 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
.index.html.erb.swp Normal file

Binary file not shown.

243
README Normal file
View File

@ -0,0 +1,243 @@
== Welcome to Rails
Rails is a web-application framework that includes everything needed to create
database-backed web applications according to the Model-View-Control pattern.
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
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: You're riding the Rails!"
4. Follow the guidelines to start developing your application
== Web Servers
By default, Rails will try to use Mongrel if it's are installed when started with script/server, otherwise Rails will use WEBrick, the webserver that ships with Ruby. But you can also use Rails
with a variety of other web servers.
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
Say other Ruby web servers like Thin and Ebb or regular web servers like Apache or LiteSpeed or
Lighttpd or IIS. The Ruby web servers are run through Rack and the latter can either be setup to use
FCGI or proxy to a pack of Mongrels/Thin/Ebb servers.
== Apache .htaccess example for FCGI/CGI
# 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.cgi [QSA,L]
# In case Rails experiences terminal errors
# Instead of displaying this message you can supply a file here which will be rendered instead
#
# Example:
# ErrorDocument 500 /500.html
ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"
== 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!
You need to install ruby-debug to run the server in debugging mode. With gems, use 'gem install ruby-debug'
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>
== dbconsole
You can go to the command line of your database directly through <tt>script/dbconsole</tt>.
You would be connected to the database with the credentials defined in database.yml.
Starting the script without arguments will connect you to the development database. Passing an
argument will connect you to a different database, like <tt>script/dbconsole production</tt>.
Currently works for mysql, postgresql and sqlite.
== 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.html.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.html.erb. Inside default.html.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.
If the app has frozen rails, those gems also go here, under vendor/rails/.
This directory is in the load path.

10
Rakefile Normal file
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'

Binary file not shown.

Binary file not shown.

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
protect_from_forgery # See ActionController::RequestForgeryProtection for details
# Scrub sensitive parameters from your log
# filter_parameter_logging :password
end

View File

@ -0,0 +1,19 @@
class PhotosController < ApplicationController
def new
@photo = Photo.new
end
def create
@photo = Photo.new(params[:photo])
if @photo.save
flash[:notice] = 'Photo was successfully saved'
redirect_to photo_url(@photo)
else
render :action => :new
end
end
def show
@photo = Photo.find(params[:id])
end
end

View File

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

View File

@ -0,0 +1,2 @@
module PhotosHelper
end

BIN
app/models/.category.rb.swp Normal file

Binary file not shown.

BIN
app/models/.category.rb.un~ Normal file

Binary file not shown.

BIN
app/models/.photo.rb.swp Normal file

Binary file not shown.

BIN
app/models/.photo.rb.un~ Normal file

Binary file not shown.

3
app/models/category.rb Normal file
View File

@ -0,0 +1,3 @@
class Category < ActiveRecord::Base
has_many :photos
end

12
app/models/photo.rb Normal file
View File

@ -0,0 +1,12 @@
class Photo < ActiveRecord::Base
belongs_to :category
has_attachment :content_type => :image,
:storage => :file_system,
:max_size => 10.megabytes,
:processor => 'ImageScience',
:resize_to => '1024x1024>',
:thumbnails => { :thumb => '140x140' }
validates_as_attachment
end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,13 @@
<%= error_messages_for :photo %>
<% form_for(:photo, :url => :photos, :html => { :multipart => true }) do |f| -%>
<p>
<%= f.label :image_file %>:<br />
<%= f.file_field :uploaded_data %>
</p>
<p>
<%= submit_tag 'Upload' %>
</p>
<% end -%>

View File

@ -0,0 +1 @@
<%= link_to image_tag(@photo.public_filename(:thumb)), @photo.public_filename %>

BIN
config/.routes.rb.swp Normal file

Binary file not shown.

BIN
config/.routes.rb.un~ Normal file

Binary file not shown.

17
config/amazon_s3.yml Normal file
View File

@ -0,0 +1,17 @@
development:
bucket_name: appname_development
access_key_id:
secret_access_key:
distribution_domain: XXXX.cloudfront.net
test:
bucket_name: appname_test
access_key_id:
secret_access_key:
distribution_domain: XXXX.cloudfront.net
production:
bucket_name: appname
access_key_id:
secret_access_key:
distribution_domain: XXXX.cloudfront.net

110
config/boot.rb Normal file
View File

@ -0,0 +1,110 @@
# 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
def preinitialize
load(preinitializer_path) if File.exist?(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"
Rails::Initializer.run(:install_gem_spec_stubs)
Rails::GemDependency.add_frozen_gem_path
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 rescue nil
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
min_version = '1.3.2'
require 'rubygems'
unless rubygems_version >= min_version
$stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
exit 1
end
rescue LoadError
$stderr.puts %Q(Rails requires RubyGems >= #{min_version}. 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!

41
config/environment.rb Normal file
View File

@ -0,0 +1,41 @@
# Be sure to restart your server when you modify this file
# Specifies gem version of Rails to use when vendor/rails is not present
RAILS_GEM_VERSION = '2.3.8' unless defined? RAILS_GEM_VERSION
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
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.
# Add additional load paths for your own custom dirs
# config.load_paths += %W( #{RAILS_ROOT}/extras )
# Specify gems that this application depends on and have them installed with rake gems:install
# config.gem "bj"
# config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
# config.gem "sqlite3-ruby", :lib => "sqlite3"
# config.gem "aws-s3", :lib => "aws/s3"
# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
# Skip frameworks you're not going to use. To use Rails without a database,
# you must remove the Active Record framework.
# config.frameworks -= [ :active_record, :active_resource, :action_mailer ]
# Activate observers that should always be running
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names.
config.time_zone = 'UTC'
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')]
# config.i18n.default_locale = :de
end

View File

@ -0,0 +1,17 @@
# 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
# Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = false

View File

@ -0,0 +1,28 @@
# 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
# 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
# See everything in the log (default is :info)
# config.log_level = :debug
# Use a different logger for distributed setups
# config.logger = SyslogLogger.new
# Use a different cache store in production
# config.cache_store = :mem_cache_store
# 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
# Enable threaded mode
# config.threadsafe!

View File

@ -0,0 +1,28 @@
# 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
config.action_view.cache_template_loading = true
# Disable request forgery protection in test environment
config.action_controller.allow_forgery_protection = false
# Tell Action Mailer 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
# 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

View File

@ -0,0 +1,7 @@
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
# You can also remove all the silencers if you're trying do debug a problem that might steem from framework code.
# Rails.backtrace_cleaner.remove_silencers!

View File

@ -0,0 +1,7 @@
# Be sure to restart your server when you modify this file.
# Your secret key for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies 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.
ActionController::Base.cookie_verifier_secret = '753efedb9ba8c926e9053ad138f162b2100fe0569940ff9b0fdea8d3ebb973c12a5028923f5557270806b20084e154ea28550234639ec67542fa49aa603454c9';

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):
# ActiveSupport::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,21 @@
# Be sure to restart your server when you modify this file.
# These settings change the behavior of Rails 2 apps and will be defaults
# for Rails 3. You can remove this initializer when Rails 3 is released.
if defined?(ActiveRecord)
# Include Active Record class name as root for JSON serialized output.
ActiveRecord::Base.include_root_in_json = true
# Store the full class name (including module namespace) in STI type column.
ActiveRecord::Base.store_full_sti_class = true
end
ActionController::Routing.generate_best_match = false
# 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,15 @@
# Be sure to restart your server when you modify this file.
# 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.
ActionController::Base.session = {
:key => '_photos_session',
:secret => 'ed6e47eac508a8c65c3c07ed65c888777d4282f081da72cfdd2b19cf8ff553a655b3fc44e6a92c85c396444359539934e1f1c2ea1558dcd0fafc80699cfc5694'
}
# 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")
# ActionController::Base.session_store = :active_record_store

5
config/locales/en.yml Normal file
View File

@ -0,0 +1,5 @@
# Sample localization file for English. Add more files in this directory for other locales.
# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
en:
hello: "Hello world"

View File

@ -0,0 +1,14 @@
development:
container_name: appname_development
username:
api_key:
test:
container_name: appname_test
username:
api_key:
production:
container_name: appname_production
username:
api_key:

46
config/routes.rb Normal file
View File

@ -0,0 +1,46 @@
ActionController::Routing::Routes.draw do |map|
map.resources :photos
# 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 with more complex sub-resources
# map.resources :products do |products|
# products.resources :comments
# products.resources :sales, :collection => { :recent => :get }
# end
# 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.
# Note: These default routes make all actions in every controller accessible via GET requests. You should
# consider removing or commenting them out if you're using named routes and resources.
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
end

BIN
db/development.sqlite3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,14 @@
class CreateCategories < ActiveRecord::Migration
def self.up
create_table :categories do |t|
t.string :name
t.text :description
t.timestamps
end
end
def self.down
drop_table :categories
end
end

View File

@ -0,0 +1,23 @@
class CreatePhotos < ActiveRecord::Migration
def self.up
create_table :photos do |t|
t.integer :category_id
t.string :flickr_url
t.integer :parent_id
t.string :content_type
t.string :filename
t.string :thumbnail
t.integer :size
t.integer :width
t.integer :height
t.timestamps
end
end
def self.down
drop_table :photos
end
end

35
db/schema.rb Normal file
View File

@ -0,0 +1,35 @@
# 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 => 20101006095457) do
create_table "categories", :force => true do |t|
t.string "name"
t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "photos", :force => true do |t|
t.integer "category_id"
t.string "flickr_url"
t.integer "parent_id"
t.string "content_type"
t.string "filename"
t.string "thumbnail"
t.integer "size"
t.integer "width"
t.integer "height"
t.datetime "created_at"
t.datetime "updated_at"
end
end

7
db/seeds.rb Normal file
View File

@ -0,0 +1,7 @@
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
#
# Examples:
#
# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
# Major.create(:name => 'Daley', :city => cities.first)

2
doc/README_FOR_APP Normal file
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.

427
log/development.log Normal file
View File

@ -0,0 +1,427 @@
SQL (0.2ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) select sqlite_version(*)
SQL (3.8ms) CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL) 
SQL (0.0ms) PRAGMA index_list("schema_migrations")
SQL (3.2ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
SQL (0.1ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.2ms) SELECT version FROM schema_migrations
Migrating to CreateCategories (20101006095323)
SQL (0.4ms) CREATE TABLE "categories" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "description" text, "created_at" datetime, "updated_at" datetime) 
SQL (0.1ms) INSERT INTO schema_migrations (version) VALUES ('20101006095323')
Migrating to CreatePhotos (20101006095457)
SQL (0.4ms) CREATE TABLE "photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "category_id" integer, "content_type" varchar(255), "filename" varchar(255), "thumbnail" varchar(255), "size" integer, "width" integer, "height" integer, "created_at" datetime, "updated_at" datetime) 
SQL (0.1ms) INSERT INTO schema_migrations (version) VALUES ('20101006095457')
SQL (0.3ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.2ms) SELECT version FROM schema_migrations
SQL (0.2ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.0ms) PRAGMA index_list("categories")
SQL (0.0ms) PRAGMA index_list("photos")
SQL (0.4ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.4ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (35.1ms) DROP TABLE "categories"
SQL (13.4ms) DELETE FROM schema_migrations WHERE version = '20101006095323'
SQL (0.2ms) select sqlite_version(*)
SQL (0.2ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (0.1ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.0ms) PRAGMA index_list("photos")
SQL (0.3ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (1.4ms) DROP TABLE "photos"
SQL (1.0ms) DELETE FROM schema_migrations WHERE version = '20101006095457'
SQL (0.2ms) select sqlite_version(*)
SQL (0.2ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (0.1ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.4ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
Migrating to CreateCategories (20101006095323)
SQL (0.1ms) select sqlite_version(*)
SQL (61.2ms) CREATE TABLE "categories" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "description" text, "created_at" datetime, "updated_at" datetime) 
SQL (0.2ms) INSERT INTO schema_migrations (version) VALUES ('20101006095323')
Migrating to CreatePhotos (20101006095457)
SQL (0.4ms) CREATE TABLE "photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "category_id" integer, "title" varchar(255), "description" text, "flickr_url" varchar(255), "content_type" varchar(255), "filename" varchar(255), "thumbnail" varchar(255), "size" integer, "width" integer, "height" integer, "created_at" datetime, "updated_at" datetime) 
SQL (0.1ms) INSERT INTO schema_migrations (version) VALUES ('20101006095457')
SQL (0.4ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (0.2ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.0ms) PRAGMA index_list("categories")
SQL (0.0ms) PRAGMA index_list("photos")
Processing ApplicationController#index (for 127.0.0.1 at 2010-10-06 15:27:36) [GET]
ActionController::RoutingError (No route matches "/photos" with {:method=>:get}):
<internal:prelude>:8:in `synchronize'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:111:in `service'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:70:in `run'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/server.rb:183:in `block in start_thread'
Rendering rescues/layout (not_found)
Processing ApplicationController#index (for 127.0.0.1 at 2010-10-06 15:28:54) [GET]
ActionController::RoutingError (No route matches "/photos" with {:method=>:get}):
<internal:prelude>:8:in `synchronize'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:111:in `service'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:70:in `run'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/server.rb:183:in `block in start_thread'
Rendering rescues/layout (not_found)
Processing ApplicationController#index (for 127.0.0.1 at 2010-10-06 15:28:56) [GET]
ActionController::RoutingError (No route matches "/photos" with {:method=>:get}):
<internal:prelude>:8:in `synchronize'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:111:in `service'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:70:in `run'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/server.rb:183:in `block in start_thread'
Rendering rescues/layout (not_found)
Processing PhotosController#index (for 127.0.0.1 at 2010-10-06 15:29:04) [GET]
ActionController::UnknownAction (No action responded to index. Actions: ):
<internal:prelude>:8:in `synchronize'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:111:in `service'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:70:in `run'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/server.rb:183:in `block in start_thread'
Rendering rescues/layout (not_found)
Processing PhotosController#new (for 127.0.0.1 at 2010-10-06 15:29:12) [GET]
ActionController::UnknownAction (No action responded to new. Actions: ):
<internal:prelude>:8:in `synchronize'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:111:in `service'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:70:in `run'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/server.rb:183:in `block in start_thread'
Rendering rescues/layout (not_found)
Processing PhotosController#new (for 127.0.0.1 at 2010-10-06 15:35:33) [GET]
Rendering photos/new
Completed in 51ms (View: 50, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-06 15:35:51) [GET]
Rendering photos/new
Completed in 40ms (View: 39, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-06 15:37:01) [GET]
Rendering photos/new
Completed in 9ms (View: 8, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 04:08:18) [GET]
Rendering photos/new
Completed in 14ms (View: 13, DB: 0) | 200 OK [http://localhost/photos/new]
SQL (0.4ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.2ms) SELECT version FROM schema_migrations
SQL (117.3ms) DROP TABLE "photos"
SQL (5.3ms) DELETE FROM schema_migrations WHERE version = '20101006095457'
SQL (0.2ms) select sqlite_version(*)
SQL (0.2ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (0.1ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.0ms) PRAGMA index_list("categories")
SQL (0.3ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.3ms) SELECT version FROM schema_migrations
Migrating to CreateCategories (20101006095323)
Migrating to CreatePhotos (20101006095457)
SQL (0.1ms) select sqlite_version(*)
SQL (0.7ms) CREATE TABLE "photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "category_id" integer, "flickr_url" varchar(255), "content_type" varchar(255), "filename" varchar(255), "thumbnail" varchar(255), "size" integer, "width" integer, "height" integer, "created_at" datetime, "updated_at" datetime) 
SQL (0.1ms) INSERT INTO schema_migrations (version) VALUES ('20101006095457')
SQL (0.3ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (0.1ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.0ms) PRAGMA index_list("categories")
SQL (0.0ms) PRAGMA index_list("photos")
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 08:14:49) [GET]
Rendering photos/new
Completed in 90ms (View: 88, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 08:14:49) [GET]
Rendering photos/new
Completed in 4ms (View: 3, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 10:16:53) [POST]
Parameters: {"authenticity_token"=>"MRMA5jN8F+L8Qu/qxudBGU2lL/82P68aMbmoNbKb5zw=", "photo"=>{"uploaded_data"=>#<File:/var/folders/B2/B2qStFgNHj4CjwOiJRPQ+++++TI/-Tmp-/RackMultipart57096-0>}, "commit"=>"Upload"}
SyntaxError (/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/gems/1.9.1/gems/gd2-1.1.1/lib/gd2.rb:46: syntax error, unexpected ':', expecting keyword_then or ',' or ';' or '\n'
when ?D: 8
^
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/gems/1.9.1/gems/gd2-1.1.1/lib/gd2.rb:180: syntax error, unexpected keyword_end, expecting $end):
app/models/photo.rb:8:in `<class:Photo>'
app/models/photo.rb:1:in `<top (required)>'
app/controllers/photos_controller.rb:3:in `new'
<internal:prelude>:8:in `synchronize'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:111:in `service'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/httpserver.rb:70:in `run'
/usr/local/Cellar/ruby/1.9.1-p378/lib/ruby/1.9.1/webrick/server.rb:183:in `block in start_thread'
Rendered rescues/_trace (113.8ms)
Rendered rescues/_request_and_response (1.2ms)
Rendering rescues/layout (internal_server_error)
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 10:17:42) [POST]
Parameters: {"photo"=>{"uploaded_data"=>#<File:/var/folders/B2/B2qStFgNHj4CjwOiJRPQ+++++TI/-Tmp-/RackMultipart57116-0>}, "commit"=>"Upload", "authenticity_token"=>"MRMA5jN8F+L8Qu/qxudBGU2lL/82P68aMbmoNbKb5zw="}
Rendering photos/new
Completed in 2700ms (View: 133, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 10:21:41) [POST]
Parameters: {"photo"=>{"uploaded_data"=>#<File:/var/folders/B2/B2qStFgNHj4CjwOiJRPQ+++++TI/-Tmp-/RackMultipart57116-1>}, "commit"=>"Upload", "authenticity_token"=>"MRMA5jN8F+L8Qu/qxudBGU2lL/82P68aMbmoNbKb5zw="}
Rendering photos/new
Completed in 176ms (View: 32, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 10:24:58) [POST]
Parameters: {"photo"=>{"uploaded_data"=>#<File:/var/folders/B2/B2qStFgNHj4CjwOiJRPQ+++++TI/-Tmp-/RackMultipart57116-1>}, "commit"=>"Upload", "authenticity_token"=>"MRMA5jN8F+L8Qu/qxudBGU2lL/82P68aMbmoNbKb5zw="}
Rendering photos/new
Completed in 50ms (View: 43, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 10:25:19) [POST]
Parameters: {"photo"=>{"uploaded_data"=>#<File:/var/folders/B2/B2qStFgNHj4CjwOiJRPQ+++++TI/-Tmp-/RackMultipart58018-0>}, "commit"=>"Upload", "authenticity_token"=>"MRMA5jN8F+L8Qu/qxudBGU2lL/82P68aMbmoNbKb5zw="}
Rendering photos/new
Completed in 131ms (View: 6, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 10:26:30) [POST]
Parameters: {"photo"=>{"uploaded_data"=>#<File:/var/folders/B2/B2qStFgNHj4CjwOiJRPQ+++++TI/-Tmp-/RackMultipart58018-1>}, "commit"=>"Upload", "authenticity_token"=>"MRMA5jN8F+L8Qu/qxudBGU2lL/82P68aMbmoNbKb5zw="}
Rendering photos/new
Completed in 10ms (View: 4, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 10:31:32) [POST]
Parameters: {"photo"=>{"uploaded_data"=>#<File:/var/folders/B2/B2qStFgNHj4CjwOiJRPQ+++++TI/-Tmp-/RackMultipart58175-0>}, "commit"=>"Upload", "authenticity_token"=>"MRMA5jN8F+L8Qu/qxudBGU2lL/82P68aMbmoNbKb5zw="}
Rendering photos/new
Completed in 119ms (View: 6, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 10:31:43) [POST]
Parameters: {"photo"=>{"uploaded_data"=>#<File:/var/folders/B2/B2qStFgNHj4CjwOiJRPQ+++++TI/-Tmp-/RackMultipart58175-1>}, "commit"=>"Upload", "authenticity_token"=>"MRMA5jN8F+L8Qu/qxudBGU2lL/82P68aMbmoNbKb5zw="}
Rendering photos/new
Completed in 10ms (View: 3, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 10:34:14) [GET]
Rendering photos/new
Completed in 13ms (View: 8, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#create (for 127.0.0.1 at 2010-10-07 10:34:24) [POST]
Parameters: {"photo"=>{"uploaded_data"=>#<File:/var/folders/B2/B2qStFgNHj4CjwOiJRPQ+++++TI/-Tmp-/RackMultipart58175-1>}, "commit"=>"Upload", "authenticity_token"=>"MRMA5jN8F+L8Qu/qxudBGU2lL/82P68aMbmoNbKb5zw="}
Photo Create (65.2ms) INSERT INTO "photos" ("size", "created_at", "content_type", "flickr_url", "thumbnail", "updated_at", "category_id", "filename", "height", "width") VALUES(271536, '2010-10-07 14:34:24', 'image/jpeg', NULL, NULL, '2010-10-07 14:34:24', NULL, 'Buttercup.jpg', 649, 1024)
Redirected to http://localhost:3000/photos/1
Completed in 857ms (DB: 65) | 302 Found [http://localhost/photos]
Processing PhotosController#show (for 127.0.0.1 at 2010-10-07 10:34:25) [GET]
Parameters: {"id"=>"1"}
ActionController::UnknownAction (No action responded to show. Actions: create and new):
Rendering rescues/layout (not_found)
Processing PhotosController#new (for 127.0.0.1 at 2010-10-07 10:48:12) [GET]
Rendering photos/new
Completed in 117ms (View: 30, DB: 0) | 200 OK [http://localhost/photos/new]
Processing PhotosController#create (for 127.0.0.1 at 2010-10-07 10:48:23) [POST]
Parameters: {"photo"=>{"uploaded_data"=>#<File:/var/folders/B2/B2qStFgNHj4CjwOiJRPQ+++++TI/-Tmp-/RackMultipart58567-0>}, "commit"=>"Upload", "authenticity_token"=>"MRMA5jN8F+L8Qu/qxudBGU2lL/82P68aMbmoNbKb5zw="}
Photo Create (46.1ms) INSERT INTO "photos" ("size", "created_at", "content_type", "flickr_url", "thumbnail", "updated_at", "category_id", "filename", "height", "width") VALUES(616171, '2010-10-07 14:48:24', 'image/jpeg', NULL, NULL, '2010-10-07 14:48:24', NULL, 'Bell_Tower.jpg', 736, 1024)
Redirected to http://localhost:3000/photos/2
Completed in 816ms (DB: 46) | 302 Found [http://localhost/photos]
Processing PhotosController#show (for 127.0.0.1 at 2010-10-07 10:48:24) [GET]
Parameters: {"id"=>"2"}
ActionController::UnknownAction (No action responded to show. Actions: create and new):
Rendering rescues/layout (not_found)
Processing PhotosController#show (for 127.0.0.1 at 2010-10-07 10:51:36) [GET]
Parameters: {"id"=>"1"}
ActionController::UnknownAction (No action responded to show. Actions: create and new):
Rendering rescues/layout (not_found)
Processing PhotosController#show (for 127.0.0.1 at 2010-10-07 10:52:48) [GET]
Parameters: {"id"=>"1"}
Photo Load (0.2ms) SELECT * FROM "photos" WHERE ("photos"."id" = 1) 
ActionView::MissingTemplate (Missing template photos/show.erb in view path app/views):
Rendering rescues/layout (internal_server_error)
Processing PhotosController#show (for 127.0.0.1 at 2010-10-07 10:53:50) [GET]
Parameters: {"id"=>"1"}
Photo Load (0.2ms) SELECT * FROM "photos" WHERE ("photos"."id" = 1) 
Rendering photos/show
Completed in 12ms (View: 5, DB: 0) | 200 OK [http://localhost/photos/1]
Processing ApplicationController#index (for 127.0.0.1 at 2010-10-07 10:53:50) [GET]
ActionController::RoutingError (No route matches "/photos/0000/0001/Buttercup_thumb.jpg" with {:method=>:get}):
Rendering rescues/layout (not_found)
Processing PhotosController#show (for 127.0.0.1 at 2010-10-07 10:54:28) [GET]
Parameters: {"id"=>"2"}
Photo Load (0.2ms) SELECT * FROM "photos" WHERE ("photos"."id" = 2) 
Rendering photos/show
Completed in 12ms (View: 3, DB: 0) | 200 OK [http://localhost/photos/2]
Processing ApplicationController#index (for 127.0.0.1 at 2010-10-07 10:54:28) [GET]
ActionController::RoutingError (No route matches "/photos/0000/0002/Bell_Tower_thumb.jpg" with {:method=>:get}):
Rendering rescues/layout (not_found)
SQL (0.4ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (24.2ms) DROP TABLE "categories"
SQL (1.0ms) DELETE FROM schema_migrations WHERE version = '20101006095323'
SQL (0.1ms) select sqlite_version(*)
SQL (0.1ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (0.1ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.0ms) PRAGMA index_list("photos")
SQL (0.3ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (1.6ms) DROP TABLE "photos"
SQL (1.0ms) DELETE FROM schema_migrations WHERE version = '20101006095457'
SQL (0.1ms) select sqlite_version(*)
SQL (0.1ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (0.1ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.3ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
Migrating to CreateCategories (20101006095323)
SQL (0.1ms) select sqlite_version(*)
SQL (0.4ms) CREATE TABLE "categories" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "description" text, "created_at" datetime, "updated_at" datetime) 
SQL (0.1ms) INSERT INTO schema_migrations (version) VALUES ('20101006095323')
Migrating to CreatePhotos (20101006095457)
SQL (0.5ms) CREATE TABLE "photos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "category_id" integer, "flickr_url" varchar(255), "parent_id" integer, "content_type" varchar(255), "filename" varchar(255), "thumbnail" varchar(255), "size" integer, "width" integer, "height" integer, "created_at" datetime, "updated_at" datetime) 
SQL (0.1ms) INSERT INTO schema_migrations (version) VALUES ('20101006095457')
SQL (0.3ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.1ms) SELECT version FROM schema_migrations
SQL (0.1ms)  SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'

SQL (0.0ms) PRAGMA index_list("categories")
SQL (0.0ms) PRAGMA index_list("photos")

0
log/production.log Normal file
View File

0
log/server.log Normal file
View File

0
log/test.log Normal file
View File

BIN
public/.DS_Store vendored Normal file

Binary file not shown.

30
public/404.html Normal file
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>

30
public/422.html Normal file
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>

30
public/500.html Normal file
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>

0
public/favicon.ico Normal file
View File

BIN
public/images/rails.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

275
public/index.html Normal file
View File

@ -0,0 +1,275 @@
<!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://wiki.rubyonrails.org/">Wiki</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>
<li><a href="http://guides.rubyonrails.org/">Rails Guides</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>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>
<li>
<h2>Create your database</h2>
<p>Run <tt>rake db:migrate</tt> to create your database. If you're not using SQLite (the default), edit <tt>config/database.yml</tt> with your username and password.</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

963
public/javascripts/controls.js vendored Normal file
View File

@ -0,0 +1,963 @@
// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005-2008 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.unescapeHTML();
},
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));
}
});

973
public/javascripts/dragdrop.js vendored Normal file
View File

@ -0,0 +1,973 @@
// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2008 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._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
if (!this._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._originallyAbsolute)
Position.relativize(this.element);
delete this._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){
element = $(element);
var s = Sortable.sortables[element.id];
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')];
};

1128
public/javascripts/effects.js vendored Normal file

File diff suppressed because it is too large Load Diff

4320
public/javascripts/prototype.js vendored Normal file

File diff suppressed because it is too large Load Diff

5
public/robots.txt Normal file
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: /

4
script/about Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require File.expand_path('../../config/boot', __FILE__)
$LOAD_PATH.unshift "#{RAILTIES_PATH}/builtin/rails_info"
require 'commands/about'

3
script/console Executable file
View File

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

3
script/dbconsole Executable file
View File

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

3
script/destroy Executable file
View File

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

3
script/generate Executable file
View File

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

3
script/performance/benchmarker Executable file
View File

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

3
script/performance/profiler Executable file
View File

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

3
script/plugin Executable file
View File

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

3
script/runner Executable file
View File

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

3
script/server Executable file
View File

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

9
test/fixtures/categories.yml vendored Normal file
View File

@ -0,0 +1,9 @@
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
one:
name: MyString
description: MyText
two:
name: MyString
description: MyText

19
test/fixtures/photos.yml vendored Normal file
View File

@ -0,0 +1,19 @@
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
one:
category_id: 1
content_type: MyString
filename: MyString
thumbnail: MyString
size: 1
width: 1
height: 1
two:
category_id: 1
content_type: MyString
filename: MyString
thumbnail: MyString
size: 1
width: 1
height: 1

View File

@ -0,0 +1,8 @@
require 'test_helper'
class PhotosControllerTest < ActionController::TestCase
# Replace this with your real tests.
test "the truth" do
assert true
end
end

View File

@ -0,0 +1,9 @@
require 'test_helper'
require 'performance_test_help'
# Profiling results for each test method are written to tmp/performance.
class BrowsingTest < ActionController::PerformanceTest
def test_homepage
get '/'
end
end

38
test/test_helper.rb Normal file
View File

@ -0,0 +1,38 @@
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'test_help'
class ActiveSupport::TestCase
# Transactional fixtures accelerate your tests by wrapping each test method
# in a transaction that's rolled back on completion. This ensures that the
# test database remains unchanged so your fixtures don't have to be reloaded
# between every test method. Fewer database queries means faster tests.
#
# Read Mike Clark's excellent walkthrough at
# http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
#
# Every Active Record database supports transactions except MyISAM tables
# in MySQL. Turn off transactional fixtures in this case; however, if you
# don't care one way or the other, switching from MyISAM to InnoDB tables
# is recommended.
#
# The only drawback to using transactional fixtures is when you actually
# need to test transactions. Since your test is bracketed by a transaction,
# any transactions started in your code will be automatically rolled back.
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where otherwise you
# would need people(:david). If you don't want to migrate your existing
# test cases which use the @david style and don't mind the speed hit (each
# instantiated fixtures translates to a database query per test method),
# then set this back to true.
self.use_instantiated_fixtures = false
# Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
#
# Note: You'll currently still have to declare fixtures explicitly in integration tests
# -- they do not yet inherit this setting
fixtures :all
# Add more helper methods to be used by all tests here...
end

View File

@ -0,0 +1,8 @@
require 'test_helper'
class CategoryTest < ActiveSupport::TestCase
# Replace this with your real tests.
test "the truth" do
assert true
end
end

View File

@ -0,0 +1,4 @@
require 'test_helper'
class PhotosHelperTest < ActionView::TestCase
end

8
test/unit/photo_test.rb Normal file
View File

@ -0,0 +1,8 @@
require 'test_helper'
class PhotoTest < ActiveSupport::TestCase
# Replace this with your real tests.
test "the truth" do
assert true
end
end

35
vendor/plugins/attachment_fu/CHANGELOG vendored Normal file
View File

@ -0,0 +1,35 @@
* Apr 17 2008 *
* amazon_s3.yml is now passed through ERB before being passed to AWS::S3 [François Beausoleil]
* Mar 22 2008 *
* Some tweaks to support Rails 2.0 and Rails 2.1 due to ActiveSupport::Callback changes.
Thanks to http://blog.methodmissing.com/2008/1/19/edge-callback-refactorings-attachment_fu/
* Feb. 26, 2008 *
* remove breakpoint from test_helper, makes test suite crazy (at least Rails 2+) [Rob Sanheim]
* make S3 test really optional [Rob Sanheim]
* Nov 27, 2007 *
* Handle properly ImageScience thumbnails resized from a gif file [Matt Aimonetti]
* Save thumbnails file size properly when using ImageScience [Matt Aimonetti]
* fixed s3 config file loading with latest versions of Rails [Matt Aimonetti]
* April 2, 2007 *
* don't copy the #full_filename to the default #temp_paths array if it doesn't exist
* add default ID partitioning for attachments
* add #binmode call to Tempfile (note: ruby should be doing this!) [Eric Beland]
* Check for current type of :thumbnails option.
* allow customization of the S3 configuration file path with the :s3_config_path option.
* Don't try to remove thumbnails if there aren't any. Closes #3 [ben stiglitz]
* BC * (before changelog)
* add default #temp_paths entry [mattly]
* add MiniMagick support to attachment_fu [Isacc]
* update #destroy_file to clear out any empty directories too [carlivar]
* fix references to S3Backend module [Hunter Hillegas]
* make #current_data public with db_file and s3 backends [ebryn]
* oops, actually svn add the files for s3 backend. [Jeffrey Hardy]
* experimental s3 support, egad, no tests.... [Jeffrey Hardy]
* doh, fix a few bad references to ActsAsAttachment [sixty4bit]

20
vendor/plugins/attachment_fu/LICENSE vendored Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2009 rick olson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

193
vendor/plugins/attachment_fu/README vendored Normal file
View File

@ -0,0 +1,193 @@
attachment-fu
=============
attachment_fu is a plugin by Rick Olson (aka technoweenie <http://techno-weenie.net>) and is the successor to acts_as_attachment. To get a basic run-through of its capabilities, check out Mike Clark's tutorial <http://clarkware.com/cgi/blosxom/2007/02/24#FileUploadFu>.
attachment_fu functionality
===========================
attachment_fu facilitates file uploads in Ruby on Rails. There are a few storage options for the actual file data, but the plugin always at a minimum stores metadata for each file in the database.
There are four storage options for files uploaded through attachment_fu:
File system
Database file
Amazon S3
Rackspace (Mosso) Cloud Files
Each method of storage many options associated with it that will be covered in the following section. Something to note, however, is that the Amazon S3 storage requires you to modify config/amazon_s3.yml, the Rackspace Cloud Files storage requires you to modify config/rackspace_cloudfiles.yml, and the Database file storage requires an extra table.
attachment_fu models
====================
For all three of these storage options a table of metadata is required. This table will contain information about the file (hence the 'meta') and its location. This table has no restrictions on naming, unlike the extra table required for database storage, which must have a table name of db_files (and by convention a model of DbFile).
In the model there are two methods made available by this plugins: has_attachment and validates_as_attachment.
has_attachment(options = {})
This method accepts the options in a hash:
:content_type # Allowed content types.
# Allows all by default. Use :image to allow all standard image types.
:min_size # Minimum size allowed.
# 1 byte is the default.
:max_size # Maximum size allowed.
# 1.megabyte is the default.
:size # Range of sizes allowed.
# (1..1.megabyte) is the default. This overrides the :min_size and :max_size options.
:resize_to # Used by RMagick to resize images.
# Pass either an array of width/height, or a geometry string.
:thumbnails # Specifies a set of thumbnails to generate.
# This accepts a hash of filename suffixes and RMagick resizing options.
# This option need only be included if you want thumbnailing.
:thumbnail_class # Set which model class to use for thumbnails.
# This current attachment class is used by default.
:path_prefix # Path to store the uploaded files in.
# Uses public/#{table_name} by default for the filesystem, and just #{table_name} for the S3 and Cloud Files backend.
# Setting this sets the :storage to :file_system.
:partition # Whether to partiton files in directories like /0000/0001/image.jpg. Default is true. Only applicable to the :file_system backend.
:storage # Specifies the storage system to use..
# Defaults to :db_file. Options are :file_system, :db_file, :s3, and :cloud_files.
:cloudfront # If using S3 for storage, this option allows for serving the files via Amazon CloudFront.
# Defaults to false.
:processor # Sets the image processor to use for resizing of the attached image.
# Options include ImageScience, Rmagick, and MiniMagick. Default is whatever is installed.
:uuid_primary_key # If your model's primary key is a 128-bit UUID in hexadecimal format, then set this to true.
:association_options # attachment_fu automatically defines associations with thumbnails with has_many and belongs_to. If there are any additional options that you want to pass to these methods, then specify them here.
Examples:
has_attachment :max_size => 1.kilobyte
has_attachment :size => 1.megabyte..2.megabytes
has_attachment :content_type => 'application/pdf'
has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain']
has_attachment :content_type => :image, :resize_to => [50,50]
has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50'
has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
has_attachment :storage => :file_system, :path_prefix => 'public/files'
has_attachment :storage => :file_system, :path_prefix => 'public/files',
:content_type => :image, :resize_to => [50,50], :partition => false
has_attachment :storage => :file_system, :path_prefix => 'public/files',
:thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
has_attachment :storage => :s3
has_attachment :store => :s3, :cloudfront => true
has_attachment :storage => :cloud_files
validates_as_attachment
This method prevents files outside of the valid range (:min_size to :max_size, or the :size range) from being saved. It does not however, halt the upload of such files. They will be uploaded into memory regardless of size before validation.
Example:
validates_as_attachment
attachment_fu migrations
========================
Fields for attachment_fu metadata tables...
in general:
size, :integer # file size in bytes
content_type, :string # mime type, ex: application/mp3
filename, :string # sanitized filename
that reference images:
height, :integer # in pixels
width, :integer # in pixels
that reference images that will be thumbnailed:
parent_id, :integer # id of parent image (on the same table, a self-referencing foreign-key).
# Only populated if the current object is a thumbnail.
thumbnail, :string # the 'type' of thumbnail this attachment record describes.
# Only populated if the current object is a thumbnail.
# Usage:
# [ In Model 'Avatar' ]
# has_attachment :content_type => :image,
# :storage => :file_system,
# :max_size => 500.kilobytes,
# :resize_to => '320x200>',
# :thumbnails => { :small => '10x10>',
# :thumb => '100x100>' }
# [ Elsewhere ]
# @user.avatar.thumbnails.first.thumbnail #=> 'small'
that reference files stored in the database (:db_file):
db_file_id, :integer # id of the file in the database (foreign key)
Field for attachment_fu db_files table:
data, :binary # binary file data, for use in database file storage
attachment_fu views
===================
There are two main views tasks that will be directly affected by attachment_fu: upload forms and displaying uploaded images.
There are two parts of the upload form that differ from typical usage.
1. Include ':multipart => true' in the html options of the form_for tag.
Example:
<% form_for(:attachment_metadata, :url => { :action => "create" }, :html => { :multipart => true }) do |form| %>
2. Use the file_field helper with :uploaded_data as the field name.
Example:
<%= form.file_field :uploaded_data %>
Displaying uploaded images is made easy by the public_filename method of the ActiveRecord attachment objects using file system, s3, and Cloud Files storage.
public_filename(thumbnail = nil)
Returns the public path to the file. If a thumbnail prefix is specified it will return the public file path to the corresponding thumbnail.
Examples:
attachment_obj.public_filename #=> /attachments/2/file.jpg
attachment_obj.public_filename(:thumb) #=> /attachments/2/file_thumb.jpg
attachment_obj.public_filename(:small) #=> /attachments/2/file_small.jpg
When serving files from database storage, doing more than simply downloading the file is beyond the scope of this document.
attachment_fu controllers
=========================
There are two considerations to take into account when using attachment_fu in controllers.
The first is when the files have no publicly accessible path and need to be downloaded through an action.
Example:
def readme
send_file '/path/to/readme.txt', :type => 'plain/text', :disposition => 'inline'
end
See the possible values for send_file for reference.
The second is when saving the file when submitted from a form.
Example in view:
<%= form.file_field :attachable, :uploaded_data %>
Example in controller:
def create
@attachable_file = AttachmentMetadataModel.new(params[:attachable])
if @attachable_file.save
flash[:notice] = 'Attachment was successfully created.'
redirect_to attachable_url(@attachable_file)
else
render :action => :new
end
end
attachement_fu scripting
====================================
You may wish to import a large number of images or attachments.
The following example shows how to upload a file from a script.
#!/usr/bin/env ./script/runner
# required to use ActionController::TestUploadedFile
require 'action_controller'
require 'action_controller/test_process.rb'
path = "./public/images/x.jpg"
# mimetype is a string like "image/jpeg". One way to get the mimetype for a given file on a UNIX system
# mimetype = `file -ib #{path}`.gsub(/\n/,"")
mimetype = "image/jpeg"
# This will "upload" the file at path and create the new model.
@attachable = AttachmentMetadataModel.new(:uploaded_data => ActionController::TestUploadedFile.new(path, mimetype))
@attachable.save

22
vendor/plugins/attachment_fu/Rakefile vendored Normal file
View File

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the attachment_fu plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the attachment_fu plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'ActsAsAttachment'
rdoc.options << '--line-numbers --inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View File

@ -0,0 +1,17 @@
development:
bucket_name: appname_development
access_key_id:
secret_access_key:
distribution_domain: XXXX.cloudfront.net
test:
bucket_name: appname_test
access_key_id:
secret_access_key:
distribution_domain: XXXX.cloudfront.net
production:
bucket_name: appname
access_key_id:
secret_access_key:
distribution_domain: XXXX.cloudfront.net

16
vendor/plugins/attachment_fu/init.rb vendored Normal file
View File

@ -0,0 +1,16 @@
require 'tempfile'
Tempfile.class_eval do
# overwrite so tempfiles use the extension of the basename. important for rmagick and image science
def make_tmpname(basename, n)
ext = nil
sprintf("%s%d-%d%s", basename.to_s.gsub(/\.\w+$/) { |s| ext = s; '' }, $$, n, ext)
end
end
require 'geometry'
ActiveRecord::Base.send(:extend, Technoweenie::AttachmentFu::ActMethods)
Technoweenie::AttachmentFu.tempfile_path = ATTACHMENT_FU_TEMPFILE_PATH if Object.const_defined?(:ATTACHMENT_FU_TEMPFILE_PATH)
FileUtils.mkdir_p Technoweenie::AttachmentFu.tempfile_path
$:.unshift(File.dirname(__FILE__) + '/vendor')

View File

@ -0,0 +1,7 @@
require 'fileutils'
s3_config = File.dirname(__FILE__) + '/../../../config/amazon_s3.yml'
FileUtils.cp File.dirname(__FILE__) + '/amazon_s3.yml.tpl', s3_config unless File.exist?(s3_config)
cloudfiles_config = File.dirname(__FILE__) + '/../../../config/rackspace_cloudfiles.yml'
FileUtils.cp File.dirname(__FILE__) + '/rackspace_cloudfiles.yml.tpl', cloudfiles_config unless File.exist?(cloudfiles_config)
puts IO.read(File.join(File.dirname(__FILE__), 'README'))

View File

@ -0,0 +1,93 @@
# This Geometry class was yanked from RMagick. However, it lets ImageMagick handle the actual change_geometry.
# Use #new_dimensions_for to get new dimensons
# Used so I can use spiffy RMagick geometry strings with ImageScience
class Geometry
# ! and @ are removed until support for them is added
FLAGS = ['', '%', '<', '>']#, '!', '@']
RFLAGS = { '%' => :percent,
'!' => :aspect,
'<' => :>,
'>' => :<,
'@' => :area }
attr_accessor :width, :height, :x, :y, :flag
def initialize(width=nil, height=nil, x=nil, y=nil, flag=nil)
# Support floating-point width and height arguments so Geometry
# objects can be used to specify Image#density= arguments.
raise ArgumentError, "width must be >= 0: #{width}" if width < 0
raise ArgumentError, "height must be >= 0: #{height}" if height < 0
@width = width.to_f
@height = height.to_f
@x = x.to_i
@y = y.to_i
@flag = flag
end
# Construct an object from a geometry string
RE = /\A(\d*)(?:x(\d+)?)?([-+]\d+)?([-+]\d+)?([%!<>@]?)\Z/
def self.from_s(str)
raise(ArgumentError, "no geometry string specified") unless str
if m = RE.match(str)
new(m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, RFLAGS[m[5]])
else
raise ArgumentError, "invalid geometry format"
end
end
# Convert object to a geometry string
def to_s
str = ''
str << "%g" % @width if @width > 0
str << 'x' if (@width > 0 || @height > 0)
str << "%g" % @height if @height > 0
str << "%+d%+d" % [@x, @y] if (@x != 0 || @y != 0)
str << FLAGS[@flag.to_i]
end
# attempts to get new dimensions for the current geometry string given these old dimensions.
# This doesn't implement the aspect flag (!) or the area flag (@). PDI
def new_dimensions_for(orig_width, orig_height)
new_width = orig_width
new_height = orig_height
case @flag
when :percent
scale_x = @width.zero? ? 100 : @width
scale_y = @height.zero? ? @width : @height
new_width = scale_x.to_f * (orig_width.to_f / 100.0)
new_height = scale_y.to_f * (orig_height.to_f / 100.0)
when :<, :>, nil
scale_factor =
if new_width.zero? || new_height.zero?
1.0
else
if @width.nonzero? && @height.nonzero?
[@width.to_f / new_width.to_f, @height.to_f / new_height.to_f].min
else
@width.nonzero? ? (@width.to_f / new_width.to_f) : (@height.to_f / new_height.to_f)
end
end
new_width = scale_factor * new_width.to_f
new_height = scale_factor * new_height.to_f
new_width = orig_width if @flag && orig_width.send(@flag, new_width)
new_height = orig_height if @flag && orig_height.send(@flag, new_height)
end
[new_width, new_height].collect! { |v| [v.round, 1].max }
end
end
class Array
# allows you to get new dimensions for the current array of dimensions with a given geometry string
#
# [50, 64] / '40>' # => [40, 51]
def /(geometry)
raise ArgumentError, "Only works with a [width, height] pair" if size != 2
raise ArgumentError, "Must pass a valid geometry string or object" unless geometry.is_a?(String) || geometry.is_a?(Geometry)
geometry = Geometry.from_s(geometry) if geometry.is_a?(String)
geometry.new_dimensions_for first, last
end
end

View File

@ -0,0 +1,514 @@
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
@@default_processors = %w(ImageScience Rmagick MiniMagick Gd2 CoreImage)
@@tempfile_path = File.join(RAILS_ROOT, 'tmp', 'attachment_fu')
@@content_types = [
'image/jpeg',
'image/pjpeg',
'image/jpg',
'image/gif',
'image/png',
'image/x-png',
'image/jpg',
'image/x-ms-bmp',
'image/bmp',
'image/x-bmp',
'image/x-bitmap',
'image/x-xbitmap',
'image/x-win-bitmap',
'image/x-windows-bmp',
'image/ms-bmp',
'application/bmp',
'application/x-bmp',
'application/x-win-bitmap',
'application/preview',
'image/jp_',
'application/jpg',
'application/x-jpg',
'image/pipeg',
'image/vnd.swiftview-jpeg',
'image/x-xbitmap',
'application/png',
'application/x-png',
'image/gi_',
'image/x-citrix-pjpeg'
]
mattr_reader :content_types, :tempfile_path, :default_processors
mattr_writer :tempfile_path
class ThumbnailError < StandardError; end
class AttachmentError < StandardError; end
module ActMethods
# Options:
# * <tt>:content_type</tt> - Allowed content types. Allows all by default. Use :image to allow all standard image types.
# * <tt>:min_size</tt> - Minimum size allowed. 1 byte is the default.
# * <tt>:max_size</tt> - Maximum size allowed. 1.megabyte is the default.
# * <tt>:size</tt> - Range of sizes allowed. (1..1.megabyte) is the default. This overrides the :min_size and :max_size options.
# * <tt>:resize_to</tt> - Used by RMagick to resize images. Pass either an array of width/height, or a geometry string.
# * <tt>:thumbnails</tt> - Specifies a set of thumbnails to generate. This accepts a hash of filename suffixes and RMagick resizing options.
# * <tt>:thumbnail_class</tt> - Set what class to use for thumbnails. This attachment class is used by default.
# * <tt>:path_prefix</tt> - path to store the uploaded files. Uses public/#{table_name} by default for the filesystem, and just #{table_name}
# for the S3 backend. Setting this sets the :storage to :file_system.
# * <tt>:storage</tt> - Use :file_system to specify the attachment data is stored with the file system. Defaults to :db_system.
# * <tt>:cloundfront</tt> - Set to true if you are using S3 storage and want to serve the files through CloudFront. You will need to
# set a distribution domain in the amazon_s3.yml config file. Defaults to false
# * <tt>:bucket_key</tt> - Use this to specify a different bucket key other than :bucket_name in the amazon_s3.yml file. This allows you to use
# different buckets for different models. An example setting would be :image_bucket and the you would need to define the name of the corresponding
# bucket in the amazon_s3.yml file.
# * <tt>:keep_profile</tt> By default image EXIF data will be stripped to minimize image size. For small thumbnails this proivides important savings. Picture quality is not affected. Set to false if you want to keep the image profile as is. ImageScience will allways keep EXIF data.
#
# Examples:
# has_attachment :max_size => 1.kilobyte
# has_attachment :size => 1.megabyte..2.megabytes
# has_attachment :content_type => 'application/pdf'
# has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain']
# has_attachment :content_type => :image, :resize_to => [50,50]
# has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50'
# has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
# has_attachment :storage => :file_system, :path_prefix => 'public/files'
# has_attachment :storage => :file_system, :path_prefix => 'public/files',
# :content_type => :image, :resize_to => [50,50]
# has_attachment :storage => :file_system, :path_prefix => 'public/files',
# :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
# has_attachment :storage => :s3
def has_attachment(options = {})
# this allows you to redefine the acts' options for each subclass, however
options[:min_size] ||= 1
options[:max_size] ||= 1.megabyte
options[:size] ||= (options[:min_size]..options[:max_size])
options[:thumbnails] ||= {}
options[:thumbnail_class] ||= self
options[:s3_access] ||= :public_read
options[:cloudfront] ||= false
options[:content_type] = [options[:content_type]].flatten.collect! { |t| t == :image ? Technoweenie::AttachmentFu.content_types : t }.flatten unless options[:content_type].nil?
unless options[:thumbnails].is_a?(Hash)
raise ArgumentError, ":thumbnails option should be a hash: e.g. :thumbnails => { :foo => '50x50' }"
end
extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods)
include InstanceMethods unless included_modules.include?(InstanceMethods)
parent_options = attachment_options || {}
# doing these shenanigans so that #attachment_options is available to processors and backends
self.attachment_options = options
attr_accessor :thumbnail_resize_options
attachment_options[:storage] ||= (attachment_options[:file_system_path] || attachment_options[:path_prefix]) ? :file_system : :db_file
attachment_options[:storage] ||= parent_options[:storage]
attachment_options[:path_prefix] ||= attachment_options[:file_system_path]
if attachment_options[:path_prefix].nil?
attachment_options[:path_prefix] = case attachment_options[:storage]
when :s3 then table_name
when :cloud_files then table_name
else File.join("public", table_name)
end
end
attachment_options[:path_prefix] = attachment_options[:path_prefix][1..-1] if options[:path_prefix].first == '/'
association_options = { :foreign_key => 'parent_id' }
if attachment_options[:association_options]
association_options.merge!(attachment_options[:association_options])
end
with_options(association_options) do |m|
m.has_many :thumbnails, :class_name => "::#{attachment_options[:thumbnail_class]}"
m.belongs_to :parent, :class_name => "::#{base_class}" unless options[:thumbnails].empty?
end
storage_mod = Technoweenie::AttachmentFu::Backends.const_get("#{options[:storage].to_s.classify}Backend")
include storage_mod unless included_modules.include?(storage_mod)
case attachment_options[:processor]
when :none, nil
processors = Technoweenie::AttachmentFu.default_processors.dup
begin
if processors.any?
attachment_options[:processor] = processors.first
processor_mod = Technoweenie::AttachmentFu::Processors.const_get("#{attachment_options[:processor].to_s.classify}Processor")
include processor_mod unless included_modules.include?(processor_mod)
end
rescue Object, Exception
raise unless load_related_exception?($!)
processors.shift
retry
end
else
begin
processor_mod = Technoweenie::AttachmentFu::Processors.const_get("#{attachment_options[:processor].to_s.classify}Processor")
include processor_mod unless included_modules.include?(processor_mod)
rescue Object, Exception
raise unless load_related_exception?($!)
puts "Problems loading #{options[:processor]}Processor: #{$!}"
end
end unless parent_options[:processor] # Don't let child override processor
end
def load_related_exception?(e) #:nodoc: implementation specific
case
when e.kind_of?(LoadError), e.kind_of?(MissingSourceFile), $!.class.name == "CompilationError"
# We can't rescue CompilationError directly, as it is part of the RubyInline library.
# We must instead rescue RuntimeError, and check the class' name.
true
else
false
end
end
private :load_related_exception?
end
module ClassMethods
delegate :content_types, :to => Technoweenie::AttachmentFu
# Performs common validations for attachment models.
def validates_as_attachment
validates_presence_of :size, :content_type, :filename
validate :attachment_attributes_valid?
end
# Returns true or false if the given content type is recognized as an image.
def image?(content_type)
content_types.include?(content_type)
end
def self.extended(base)
base.class_inheritable_accessor :attachment_options
base.before_destroy :destroy_thumbnails
base.before_validation :set_size_from_temp_path
base.after_save :after_process_attachment
base.after_destroy :destroy_file
base.after_validation :process_attachment
if defined?(::ActiveSupport::Callbacks)
base.define_callbacks :after_resize, :after_attachment_saved, :before_thumbnail_saved
end
end
unless defined?(::ActiveSupport::Callbacks)
# Callback after an image has been resized.
#
# class Foo < ActiveRecord::Base
# acts_as_attachment
# after_resize do |record, img|
# record.aspect_ratio = img.columns.to_f / img.rows.to_f
# end
# end
def after_resize(&block)
write_inheritable_array(:after_resize, [block])
end
# Callback after an attachment has been saved either to the file system or the DB.
# Only called if the file has been changed, not necessarily if the record is updated.
#
# class Foo < ActiveRecord::Base
# acts_as_attachment
# after_attachment_saved do |record|
# ...
# end
# end
def after_attachment_saved(&block)
write_inheritable_array(:after_attachment_saved, [block])
end
# Callback before a thumbnail is saved. Use this to pass any necessary extra attributes that may be required.
#
# class Foo < ActiveRecord::Base
# acts_as_attachment
# before_thumbnail_saved do |thumbnail|
# record = thumbnail.parent
# ...
# end
# end
def before_thumbnail_saved(&block)
write_inheritable_array(:before_thumbnail_saved, [block])
end
end
# Get the thumbnail class, which is the current attachment class by default.
# Configure this with the :thumbnail_class option.
def thumbnail_class
attachment_options[:thumbnail_class] = attachment_options[:thumbnail_class].constantize unless attachment_options[:thumbnail_class].is_a?(Class)
attachment_options[:thumbnail_class]
end
# Copies the given file path to a new tempfile, returning the closed tempfile.
def copy_to_temp_file(file, temp_base_name)
returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp|
tmp.close
FileUtils.cp file, tmp.path
end
end
# Writes the given data to a new tempfile, returning the closed tempfile.
def write_to_temp_file(data, temp_base_name)
returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp|
tmp.binmode
tmp.write data
tmp.close
end
end
end
module InstanceMethods
def self.included(base)
base.define_callbacks *[:after_resize, :after_attachment_saved, :before_thumbnail_saved] if base.respond_to?(:define_callbacks)
end
# Checks whether the attachment's content type is an image content type
def image?
self.class.image?(content_type)
end
# Returns true/false if an attachment is thumbnailable. A thumbnailable attachment has an image content type and the parent_id attribute.
def thumbnailable?
image? && respond_to?(:parent_id) && parent_id.nil?
end
# Returns the class used to create new thumbnails for this attachment.
def thumbnail_class
self.class.thumbnail_class
end
# Gets the thumbnail name for a filename. 'foo.jpg' becomes 'foo_thumbnail.jpg'
def thumbnail_name_for(thumbnail = nil)
return filename if thumbnail.blank?
ext = nil
basename = filename.gsub /\.\w+$/ do |s|
ext = s; ''
end
# ImageScience doesn't create gif thumbnails, only pngs
ext.sub!(/gif$/, 'png') if attachment_options[:processor] == "ImageScience"
"#{basename}_#{thumbnail}#{ext}"
end
# Creates or updates the thumbnail for the current attachment.
def create_or_update_thumbnail(temp_file, file_name_suffix, *size)
thumbnailable? || raise(ThumbnailError.new("Can't create a thumbnail if the content type is not an image or there is no parent_id column"))
returning find_or_initialize_thumbnail(file_name_suffix) do |thumb|
thumb.temp_paths.unshift temp_file
thumb.send(:'attributes=', {
:content_type => content_type,
:filename => thumbnail_name_for(file_name_suffix),
:thumbnail_resize_options => size
}, false)
callback_with_args :before_thumbnail_saved, thumb
thumb.save!
end
end
# Sets the content type.
def content_type=(new_type)
write_attribute :content_type, new_type.to_s.strip
end
# Sanitizes a filename.
def filename=(new_name)
write_attribute :filename, sanitize_filename(new_name)
end
# Returns the width/height in a suitable format for the image_tag helper: (100x100)
def image_size
[width.to_s, height.to_s] * 'x'
end
# Returns true if the attachment data will be written to the storage system on the next save
def save_attachment?
File.file?(temp_path.to_s)
end
# nil placeholder in case this field is used in a form.
def uploaded_data() nil; end
# This method handles the uploaded file object. If you set the field name to uploaded_data, you don't need
# any special code in your controller.
#
# <% form_for :attachment, :html => { :multipart => true } do |f| -%>
# <p><%= f.file_field :uploaded_data %></p>
# <p><%= submit_tag :Save %>
# <% end -%>
#
# @attachment = Attachment.create! params[:attachment]
#
# TODO: Allow it to work with Merb tempfiles too.
def uploaded_data=(file_data)
if file_data.respond_to?(:content_type)
return nil if file_data.size == 0
self.content_type = file_data.content_type
self.filename = file_data.original_filename if respond_to?(:filename)
else
return nil if file_data.blank? || file_data['size'] == 0
self.content_type = file_data['content_type']
self.filename = file_data['filename']
file_data = file_data['tempfile']
end
if file_data.is_a?(StringIO)
file_data.rewind
set_temp_data file_data.read
else
self.temp_paths.unshift file_data
end
end
# Gets the latest temp path from the collection of temp paths. While working with an attachment,
# multiple Tempfile objects may be created for various processing purposes (resizing, for example).
# An array of all the tempfile objects is stored so that the Tempfile instance is held on to until
# it's not needed anymore. The collection is cleared after saving the attachment.
def temp_path
p = temp_paths.first
p.respond_to?(:path) ? p.path : p.to_s
end
# Gets an array of the currently used temp paths. Defaults to a copy of #full_filename.
def temp_paths
@temp_paths ||= (new_record? || !respond_to?(:full_filename) || !File.exist?(full_filename) ?
[] : [copy_to_temp_file(full_filename)])
end
# Gets the data from the latest temp file. This will read the file into memory.
def temp_data
save_attachment? ? File.read(temp_path) : nil
end
# Writes the given data to a Tempfile and adds it to the collection of temp files.
def set_temp_data(data)
temp_paths.unshift write_to_temp_file data unless data.nil?
end
# Copies the given file to a randomly named Tempfile.
def copy_to_temp_file(file)
self.class.copy_to_temp_file file, random_tempfile_filename
end
# Writes the given file to a randomly named Tempfile.
def write_to_temp_file(data)
self.class.write_to_temp_file data, random_tempfile_filename
end
# Stub for creating a temp file from the attachment data. This should be defined in the backend module.
def create_temp_file() end
# Allows you to work with a processed representation (RMagick, ImageScience, etc) of the attachment in a block.
#
# @attachment.with_image do |img|
# self.data = img.thumbnail(100, 100).to_blob
# end
#
def with_image(&block)
self.class.with_image(temp_path, &block)
end
protected
# Generates a unique filename for a Tempfile.
def random_tempfile_filename
"#{rand Time.now.to_i}#{filename || 'attachment'}"
end
def sanitize_filename(filename)
return unless filename
returning filename.strip do |name|
# NOTE: File.basename doesn't work right with Windows paths on Unix
# get only the filename, not the whole path
name.gsub! /^.*(\\|\/)/, ''
# Finally, replace all non alphanumeric, underscore or periods with underscore
name.gsub! /[^A-Za-z0-9\.\-]/, '_'
end
end
# before_validation callback.
def set_size_from_temp_path
self.size = File.size(temp_path) if save_attachment?
end
# validates the size and content_type attributes according to the current model's options
def attachment_attributes_valid?
[:size, :content_type].each do |attr_name|
enum = attachment_options[attr_name]
if Object.const_defined?(:I18n) # Rails >= 2.2
errors.add attr_name, I18n.translate("activerecord.errors.messages.inclusion", attr_name => enum) unless enum.nil? || enum.include?(send(attr_name))
else
errors.add attr_name, ActiveRecord::Errors.default_error_messages[:inclusion] unless enum.nil? || enum.include?(send(attr_name))
end
end
end
# Initializes a new thumbnail with the given suffix.
def find_or_initialize_thumbnail(file_name_suffix)
respond_to?(:parent_id) ?
thumbnail_class.find_or_initialize_by_thumbnail_and_parent_id(file_name_suffix.to_s, id) :
thumbnail_class.find_or_initialize_by_thumbnail(file_name_suffix.to_s)
end
# Stub for a #process_attachment method in a processor
def process_attachment
@saved_attachment = save_attachment?
end
# Cleans up after processing. Thumbnails are created, the attachment is stored to the backend, and the temp_paths are cleared.
def after_process_attachment
if @saved_attachment
if respond_to?(:process_attachment_with_processing) && thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil?
temp_file = temp_path || create_temp_file
attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(temp_file, suffix, *size) }
end
save_to_storage
@temp_paths.clear
@saved_attachment = nil
callback :after_attachment_saved
end
end
# Resizes the given processed img object with either the attachment resize options or the thumbnail resize options.
def resize_image_or_thumbnail!(img)
if (!respond_to?(:parent_id) || parent_id.nil?) && attachment_options[:resize_to] # parent image
resize_image(img, attachment_options[:resize_to])
elsif thumbnail_resize_options # thumbnail
resize_image(img, thumbnail_resize_options)
end
end
# Yanked from ActiveRecord::Callbacks, modified so I can pass args to the callbacks besides self.
# Only accept blocks, however
if ActiveSupport.const_defined?(:Callbacks)
# Rails 2.1 and beyond!
def callback_with_args(method, arg = self)
notify(method)
result = run_callbacks(method, { :object => arg }) { |result, object| result == false }
if result != false && respond_to_without_attributes?(method)
result = send(method)
end
result
end
def run_callbacks(kind, options = {}, &block)
options.reverse_merge!( :object => self )
self.class.send("#{kind}_callback_chain").run(options[:object], options, &block)
end
else
# Rails 2.0
def callback_with_args(method, arg = self)
notify(method)
result = nil
callbacks_for(method).each do |callback|
result = callback.call(self, arg)
return false if result == false
end
result
end
end
# Removes the thumbnails for the attachment, if it has any
def destroy_thumbnails
self.thumbnails.each { |thumbnail| thumbnail.destroy } if thumbnailable?
end
end
end
end

View File

@ -0,0 +1,211 @@
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
module Backends
# = CloudFiles Storage Backend
#
# Enables use of {Rackspace Cloud Files}[http://www.mosso.com/cloudfiles.jsp] as a storage mechanism
#
# Based heavily on the Amazon S3 backend.
#
# == Requirements
#
# Requires the {Cloud Files Gem}[http://www.mosso.com/cloudfiles.jsp] by Rackspace
#
# == Configuration
#
# Configuration is done via <tt>RAILS_ROOT/config/rackspace_cloudfiles.yml</tt> and is loaded according to the <tt>RAILS_ENV</tt>.
# The minimum connection options that you must specify are a container name, your Mosso login name and your Mosso API key.
# You can sign up for Cloud Files and get access keys by visiting https://www.mosso.com/buy.htm
#
# Example configuration (RAILS_ROOT/config/rackspace_cloudfiles.yml)
#
# development:
# container_name: appname_development
# username: <your key>
# api_key: <your key>
#
# test:
# container_name: appname_test
# username: <your key>
# api_key: <your key>
#
# production:
# container_name: appname
# username: <your key>
# apik_key: <your key>
#
# You can change the location of the config path by passing a full path to the :cloudfiles_config_path option.
#
# has_attachment :storage => :cloud_files, :cloudfiles_config_path => (RAILS_ROOT + '/config/mosso.yml')
#
# === Required configuration parameters
#
# * <tt>:username</tt> - The username for your Rackspace Cloud (Mosso) account. Provided by Rackspace.
# * <tt>:secret_access_key</tt> - The api key for your Rackspace Cloud account. Provided by Rackspace.
# * <tt>:container_name</tt> - The name of a container in your Cloud Files account.
#
# If any of these required arguments is missing, a AuthenticationException will be raised from CloudFiles::Connection.
#
# == Usage
#
# To specify Cloud Files as the storage mechanism for a model, set the acts_as_attachment <tt>:storage</tt> option to <tt>:cloud_files/tt>.
#
# class Photo < ActiveRecord::Base
# has_attachment :storage => :cloud_files
# end
#
# === Customizing the path
#
# By default, files are prefixed using a pseudo hierarchy in the form of <tt>:table_name/:id</tt>, which results
# in Cloud Files object names (and urls) that look like: http://:server/:container_name/:table_name/:id/:filename with :table_name
# representing the customizable portion of the path. You can customize this prefix using the <tt>:path_prefix</tt>
# option:
#
# class Photo < ActiveRecord::Base
# has_attachment :storage => :cloud_files, :path_prefix => 'my/custom/path'
# end
#
# Which would result in public URLs like <tt>http(s)://:server/:container_name/my/custom/path/:id/:filename.</tt>
#
# === Permissions
#
# File permisisons are determined by the permissions of the container. At present, the options are public (and distributed
# by the Limelight CDN), and private (only available to your login)
#
# === Other options
#
# Of course, all the usual configuration options apply, such as content_type and thumbnails:
#
# class Photo < ActiveRecord::Base
# has_attachment :storage => :cloud_files, :content_type => ['application/pdf', :image], :resize_to => 'x50'
# has_attachment :storage => :cloud_files, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
# end
#
# === Accessing Cloud Files URLs
#
# You can get an object's public URL using the cloudfiles_url accessor. For example, assuming that for your postcard app
# you had a container name like 'postcard_world_development', and an attachment model called Photo:
#
# @postcard.cloudfiles_url # => http://cdn.cloudfiles.mosso.com/c45182/uploaded_files/20/london.jpg
#
# The resulting url is in the form: http://:server/:container_name/:table_name/:id/:file.
# The optional thumbnail argument will output the thumbnail's filename (if any).
#
# Additionally, you can get an object's base path relative to the container root using
# <tt>base_path</tt>:
#
# @photo.file_base_path # => uploaded_files/20
#
# And the full path (including the filename) using <tt>full_filename</tt>:
#
# @photo.full_filename # => uploaded_files/20/london.jpg
#
# Niether <tt>base_path</tt> or <tt>full_filename</tt> include the container name as part of the path.
# You can retrieve the container name using the <tt>container_name</tt> method.
module CloudFileBackend
class RequiredLibraryNotFoundError < StandardError; end
class ConfigFileNotFoundError < StandardError; end
def self.included(base) #:nodoc:
mattr_reader :container_name, :cloudfiles_config
begin
require 'cloudfiles'
rescue LoadError
raise RequiredLibraryNotFoundError.new('CloudFiles could not be loaded')
end
begin
@@cloudfiles_config_path = base.attachment_options[:cloudfiles_config_path] || (RAILS_ROOT + '/config/rackspace_cloudfiles.yml')
@@cloudfiles_config = @@cloudfiles_config = YAML.load(ERB.new(File.read(@@cloudfiles_config_path)).result)[RAILS_ENV].symbolize_keys
rescue
#raise ConfigFileNotFoundError.new('File %s not found' % @@cloudfiles_config_path)
end
@@container_name = @@cloudfiles_config[:container_name]
@@cf = CloudFiles::Connection.new(@@cloudfiles_config[:username], @@cloudfiles_config[:api_key])
@@container = @@cf.container(@@container_name)
base.before_update :rename_file
end
# Overwrites the base filename writer in order to store the old filename
def filename=(value)
@old_filename = filename unless filename.nil? || @old_filename
write_attribute :filename, sanitize_filename(value)
end
# The attachment ID used in the full path of a file
def attachment_path_id
((respond_to?(:parent_id) && parent_id) || id).to_s
end
# The pseudo hierarchy containing the file relative to the container name
# Example: <tt>:table_name/:id</tt>
def base_path
File.join(attachment_options[:path_prefix], attachment_path_id)
end
# The full path to the file relative to the container name
# Example: <tt>:table_name/:id/:filename</tt>
def full_filename(thumbnail = nil)
File.join(base_path, thumbnail_name_for(thumbnail))
end
# All public objects are accessible via a GET request to the Cloud Files servers. You can generate a
# url for an object using the cloudfiles_url method.
#
# @photo.cloudfiles_url
#
# The resulting url is in the CDN URL for the object
#
# The optional thumbnail argument will output the thumbnail's filename (if any).
#
# If you are trying to get the URL for a nonpublic container, nil will be returned.
def cloudfiles_url(thumbnail = nil)
if @@container.public?
File.join(@@container.cdn_url, full_filename(thumbnail))
else
nil
end
end
alias :public_filename :cloudfiles_url
def create_temp_file
write_to_temp_file current_data
end
def current_data
@@container.get_object(full_filename).data
end
protected
# Called in the after_destroy callback
def destroy_file
@@container.delete_object(full_filename)
end
def rename_file
# Cloud Files doesn't rename right now, so we'll just nuke.
return unless @old_filename && @old_filename != filename
old_full_filename = File.join(base_path, @old_filename)
@@container.delete_object(old_full_filename)
@old_filename = nil
true
end
def save_to_storage
if save_attachment?
@object = @@container.create_object(full_filename)
@object.write((temp_path ? File.open(temp_path) : temp_data))
end
@old_filename = nil
true
end
end
end
end
end

View File

@ -0,0 +1,39 @@
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
module Backends
# Methods for DB backed attachments
module DbFileBackend
def self.included(base) #:nodoc:
Object.const_set(:DbFile, Class.new(ActiveRecord::Base)) unless Object.const_defined?(:DbFile)
base.belongs_to :db_file, :class_name => '::DbFile', :foreign_key => 'db_file_id'
end
# Creates a temp file with the current db data.
def create_temp_file
write_to_temp_file current_data
end
# Gets the current data from the database
def current_data
db_file.data
end
protected
# Destroys the file. Called in the after_destroy callback
def destroy_file
db_file.destroy if db_file
end
# Saves the data to the DbFile model
def save_to_storage
if save_attachment?
(db_file || build_db_file).data = temp_data
db_file.save!
self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id]
end
true
end
end
end
end
end

View File

@ -0,0 +1,126 @@
require 'fileutils'
require 'digest/sha2'
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
module Backends
# Methods for file system backed attachments
module FileSystemBackend
def self.included(base) #:nodoc:
base.before_update :rename_file
end
# Gets the full path to the filename in this format:
#
# # This assumes a model name like MyModel
# # public/#{table_name} is the default filesystem path
# RAILS_ROOT/public/my_models/5/blah.jpg
#
# Overwrite this method in your model to customize the filename.
# The optional thumbnail argument will output the thumbnail's filename.
def full_filename(thumbnail = nil)
file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
File.join(RAILS_ROOT, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail)))
end
# Used as the base path that #public_filename strips off full_filename to create the public path
def base_path
@base_path ||= File.join(RAILS_ROOT, 'public')
end
# The attachment ID used in the full path of a file
def attachment_path_id
((respond_to?(:parent_id) && parent_id) || id) || 0
end
# Partitions the given path into an array of path components.
#
# For example, given an <tt>*args</tt> of ["foo", "bar"], it will return
# <tt>["0000", "0001", "foo", "bar"]</tt> (assuming that that id returns 1).
#
# If the id is not an integer, then path partitioning will be performed by
# hashing the string value of the id with SHA-512, and splitting the result
# into 4 components. If the id a 128-bit UUID (as set by :uuid_primary_key => true)
# then it will be split into 2 components.
#
# To turn this off entirely, set :partition => false.
def partitioned_path(*args)
if respond_to?(:attachment_options) && attachment_options[:partition] == false
args
elsif attachment_options[:uuid_primary_key]
# Primary key is a 128-bit UUID in hex format. Split it into 2 components.
path_id = attachment_path_id.to_s
component1 = path_id[0..15] || "-"
component2 = path_id[16..-1] || "-"
[component1, component2] + args
else
path_id = attachment_path_id
if path_id.is_a?(Integer)
# Primary key is an integer. Split it after padding it with 0.
("%08d" % path_id).scan(/..../) + args
else
# Primary key is a String. Hash it, then split it into 4 components.
hash = Digest::SHA512.hexdigest(path_id.to_s)
[hash[0..31], hash[32..63], hash[64..95], hash[96..127]] + args
end
end
end
# Gets the public path to the file
# The optional thumbnail argument will output the thumbnail's filename.
def public_filename(thumbnail = nil)
full_filename(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), ''
end
def filename=(value)
@old_filename = full_filename unless filename.nil? || @old_filename
write_attribute :filename, sanitize_filename(value)
end
# Creates a temp file from the currently saved file.
def create_temp_file
copy_to_temp_file full_filename
end
protected
# Destroys the file. Called in the after_destroy callback
def destroy_file
FileUtils.rm full_filename
# remove directory also if it is now empty
Dir.rmdir(File.dirname(full_filename)) if (Dir.entries(File.dirname(full_filename))-['.','..']).empty?
rescue
logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}"
logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n")
end
# Renames the given file before saving
def rename_file
return unless @old_filename && @old_filename != full_filename
if save_attachment? && File.exists?(@old_filename)
FileUtils.rm @old_filename
elsif File.exists?(@old_filename)
FileUtils.mv @old_filename, full_filename
end
@old_filename = nil
true
end
# Saves the file to the file system
def save_to_storage
if save_attachment?
# TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
FileUtils.mkdir_p(File.dirname(full_filename))
FileUtils.cp(temp_path, full_filename)
FileUtils.chmod(attachment_options[:chmod] || 0644, full_filename)
end
@old_filename = nil
true
end
def current_data
File.file?(full_filename) ? File.read(full_filename) : nil
end
end
end
end
end

View File

@ -0,0 +1,394 @@
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
module Backends
# = AWS::S3 Storage Backend
#
# Enables use of {Amazon's Simple Storage Service}[http://aws.amazon.com/s3] as a storage mechanism
#
# == Requirements
#
# Requires the {AWS::S3 Library}[http://amazon.rubyforge.org] for S3 by Marcel Molina Jr. installed either
# as a gem or a as a Rails plugin.
#
# == Configuration
#
# Configuration is done via <tt>RAILS_ROOT/config/amazon_s3.yml</tt> and is loaded according to the <tt>RAILS_ENV</tt>.
# The minimum connection options that you must specify are a bucket name, your access key id and your secret access key.
# If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon.
# You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3.
#
# If you wish to use Amazon CloudFront to serve the files, you can also specify a distibution domain for the bucket.
# To read more about CloudFront, visit http://aws.amazon.com/cloudfront
#
# Example configuration (RAILS_ROOT/config/amazon_s3.yml)
#
# development:
# bucket_name: appname_development
# access_key_id: <your key>
# secret_access_key: <your key>
# distribution_domain: XXXX.cloudfront.net
#
# test:
# bucket_name: appname_test
# access_key_id: <your key>
# secret_access_key: <your key>
# distribution_domain: XXXX.cloudfront.net
#
# production:
# bucket_name: appname
# access_key_id: <your key>
# secret_access_key: <your key>
# distribution_domain: XXXX.cloudfront.net
#
# You can change the location of the config path by passing a full path to the :s3_config_path option.
#
# has_attachment :storage => :s3, :s3_config_path => (RAILS_ROOT + '/config/s3.yml')
#
# === Required configuration parameters
#
# * <tt>:access_key_id</tt> - The access key id for your S3 account. Provided by Amazon.
# * <tt>:secret_access_key</tt> - The secret access key for your S3 account. Provided by Amazon.
# * <tt>:bucket_name</tt> - A unique bucket name (think of the bucket_name as being like a database name).
#
# If any of these required arguments is missing, a MissingAccessKey exception will be raised from AWS::S3.
#
# == About bucket names
#
# Bucket names have to be globaly unique across the S3 system. And you can only have up to 100 of them,
# so it's a good idea to think of a bucket as being like a database, hence the correspondance in this
# implementation to the development, test, and production environments.
#
# The number of objects you can store in a bucket is, for all intents and purposes, unlimited.
#
# === Optional configuration parameters
#
# * <tt>:server</tt> - The server to make requests to. Defaults to <tt>s3.amazonaws.com</tt>.
# * <tt>:port</tt> - The port to the requests should be made on. Defaults to 80 or 443 if <tt>:use_ssl</tt> is set.
# * <tt>:use_ssl</tt> - If set to true, <tt>:port</tt> will be implicitly set to 443, unless specified otherwise. Defaults to false.
# * <tt>:distribution_domain</tt> - The CloudFront distribution domain for the bucket. This can either be the assigned
# distribution domain (ie. XXX.cloudfront.net) or a chosen domain using a CNAME. See CloudFront for more details.
#
# == Usage
#
# To specify S3 as the storage mechanism for a model, set the acts_as_attachment <tt>:storage</tt> option to <tt>:s3</tt>.
#
# class Photo < ActiveRecord::Base
# has_attachment :storage => :s3
# end
#
# === Customizing the path
#
# By default, files are prefixed using a pseudo hierarchy in the form of <tt>:table_name/:id</tt>, which results
# in S3 urls that look like: http(s)://:server/:bucket_name/:table_name/:id/:filename with :table_name
# representing the customizable portion of the path. You can customize this prefix using the <tt>:path_prefix</tt>
# option:
#
# class Photo < ActiveRecord::Base
# has_attachment :storage => :s3, :path_prefix => 'my/custom/path'
# end
#
# Which would result in URLs like <tt>http(s)://:server/:bucket_name/my/custom/path/:id/:filename.</tt>
#
# === Using different bucket names on different models
#
# By default the bucket name that the file will be stored to is the one specified by the
# <tt>:bucket_name</tt> key in the amazon_s3.yml file. You can use the <tt>:bucket_key</tt> option
# to overide this behavior on a per model basis. For instance if you want a bucket that will hold
# only Photos you can do this:
#
# class Photo < ActiveRecord::Base
# has_attachment :storage => :s3, :bucket_key => :photo_bucket_name
# end
#
# And then your amazon_s3.yml file needs to look like this.
#
# development:
# bucket_name: appname_development
# access_key_id: <your key>
# secret_access_key: <your key>
#
# test:
# bucket_name: appname_test
# access_key_id: <your key>
# secret_access_key: <your key>
#
# production:
# bucket_name: appname
# photo_bucket_name: appname_photos
# access_key_id: <your key>
# secret_access_key: <your key>
#
# If the bucket_key you specify is not there in a certain environment then attachment_fu will
# default to the <tt>bucket_name</tt> key. This way you only have to create special buckets
# this can be helpful if you only need special buckets in certain environments.
#
# === Permissions
#
# By default, files are stored on S3 with public access permissions. You can customize this using
# the <tt>:s3_access</tt> option to <tt>has_attachment</tt>. Available values are
# <tt>:private</tt>, <tt>:public_read_write</tt>, and <tt>:authenticated_read</tt>.
#
# === Other options
#
# Of course, all the usual configuration options apply, such as content_type and thumbnails:
#
# class Photo < ActiveRecord::Base
# has_attachment :storage => :s3, :content_type => ['application/pdf', :image], :resize_to => 'x50'
# has_attachment :storage => :s3, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
# end
#
# === Accessing S3 URLs
#
# You can get an object's URL using the s3_url accessor. For example, assuming that for your postcard app
# you had a bucket name like 'postcard_world_development', and an attachment model called Photo:
#
# @postcard.s3_url # => http(s)://s3.amazonaws.com/postcard_world_development/photos/1/mexico.jpg
#
# The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file.
# The optional thumbnail argument will output the thumbnail's filename (if any).
#
# Additionally, you can get an object's base path relative to the bucket root using
# <tt>base_path</tt>:
#
# @photo.file_base_path # => photos/1
#
# And the full path (including the filename) using <tt>full_filename</tt>:
#
# @photo.full_filename # => photos/
#
# Niether <tt>base_path</tt> or <tt>full_filename</tt> include the bucket name as part of the path.
# You can retrieve the bucket name using the <tt>bucket_name</tt> method.
#
# === Accessing CloudFront URLs
#
# You can get an object's CloudFront URL using the cloudfront_url accessor. Using the example from above:
# @postcard.cloudfront_url # => http://XXXX.cloudfront.net/photos/1/mexico.jpg
#
# The resulting url is in the form: http://:distribution_domain/:table_name/:id/:file
#
# If you set :cloudfront to true in your model, the public_filename will be the CloudFront
# URL, not the S3 URL.
module S3Backend
class RequiredLibraryNotFoundError < StandardError; end
class ConfigFileNotFoundError < StandardError; end
def self.included(base) #:nodoc:
mattr_reader :bucket_name, :s3_config
begin
require 'aws/s3'
include AWS::S3
rescue LoadError
raise RequiredLibraryNotFoundError.new('AWS::S3 could not be loaded')
end
begin
@@s3_config_path = base.attachment_options[:s3_config_path] || (RAILS_ROOT + '/config/amazon_s3.yml')
@@s3_config = @@s3_config = YAML.load(ERB.new(File.read(@@s3_config_path)).result)[RAILS_ENV].symbolize_keys
#rescue
# raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path)
end
bucket_key = base.attachment_options[:bucket_key]
if bucket_key and s3_config[bucket_key.to_sym]
eval_string = "def bucket_name()\n \"#{s3_config[bucket_key.to_sym]}\"\nend"
else
eval_string = "def bucket_name()\n \"#{s3_config[:bucket_name]}\"\nend"
end
base.class_eval(eval_string, __FILE__, __LINE__)
Base.establish_connection!(s3_config.slice(:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent, :proxy))
# Bucket.create(@@bucket_name)
base.before_update :rename_file
end
def self.protocol
@protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://'
end
def self.hostname
@hostname ||= s3_config[:server] || AWS::S3::DEFAULT_HOST
end
def self.port_string
@port_string ||= (s3_config[:port].nil? || s3_config[:port] == (s3_config[:use_ssl] ? 443 : 80)) ? '' : ":#{s3_config[:port]}"
end
def self.distribution_domain
@distribution_domain = s3_config[:distribution_domain]
end
module ClassMethods
def s3_protocol
Technoweenie::AttachmentFu::Backends::S3Backend.protocol
end
def s3_hostname
Technoweenie::AttachmentFu::Backends::S3Backend.hostname
end
def s3_port_string
Technoweenie::AttachmentFu::Backends::S3Backend.port_string
end
def cloudfront_distribution_domain
Technoweenie::AttachmentFu::Backends::S3Backend.distribution_domain
end
end
# Overwrites the base filename writer in order to store the old filename
def filename=(value)
@old_filename = filename unless filename.nil? || @old_filename
write_attribute :filename, sanitize_filename(value)
end
# The attachment ID used in the full path of a file
def attachment_path_id
((respond_to?(:parent_id) && parent_id) || id).to_s
end
# The pseudo hierarchy containing the file relative to the bucket name
# Example: <tt>:table_name/:id</tt>
def base_path
File.join(attachment_options[:path_prefix], attachment_path_id)
end
# The full path to the file relative to the bucket name
# Example: <tt>:table_name/:id/:filename</tt>
def full_filename(thumbnail = nil)
File.join(base_path, thumbnail_name_for(thumbnail))
end
# All public objects are accessible via a GET request to the S3 servers. You can generate a
# url for an object using the s3_url method.
#
# @photo.s3_url
#
# The resulting url is in the form: <tt>http(s)://:server/:bucket_name/:table_name/:id/:file</tt> where
# the <tt>:server</tt> variable defaults to <tt>AWS::S3 URL::DEFAULT_HOST</tt> (s3.amazonaws.com) and can be
# set using the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
#
# The optional thumbnail argument will output the thumbnail's filename (if any).
def s3_url(thumbnail = nil)
File.join(s3_protocol + s3_hostname + s3_port_string, bucket_name, full_filename(thumbnail))
end
# All public objects are accessible via a GET request to CloudFront. You can generate a
# url for an object using the cloudfront_url method.
#
# @photo.cloudfront_url
#
# The resulting url is in the form: <tt>http://:distribution_domain/:table_name/:id/:file</tt> using
# the <tt>:distribution_domain</tt> variable set in the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
#
# The optional thumbnail argument will output the thumbnail's filename (if any).
def cloudfront_url(thumbnail = nil)
"http://" + cloudfront_distribution_domain + "/" + full_filename(thumbnail)
end
def public_filename(*args)
if attachment_options[:cloudfront]
cloudfront_url(args)
else
s3_url(args)
end
end
# All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an
# authenticated url for an object like this:
#
# @photo.authenticated_s3_url
#
# By default authenticated urls expire 5 minutes after they were generated.
#
# Expiration options can be specified either with an absolute time using the <tt>:expires</tt> option,
# or with a number of seconds relative to now with the <tt>:expires_in</tt> option:
#
# # Absolute expiration date (October 13th, 2025)
# @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i)
#
# # Expiration in five hours from now
# @photo.authenticated_s3_url(:expires_in => 5.hours)
#
# You can specify whether the url should go over SSL with the <tt>:use_ssl</tt> option.
# By default, the ssl settings for the current connection will be used:
#
# @photo.authenticated_s3_url(:use_ssl => true)
#
# Finally, the optional thumbnail argument will output the thumbnail's filename (if any):
#
# @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true)
def authenticated_s3_url(*args)
options = args.extract_options!
options[:expires_in] = options[:expires_in].to_i if options[:expires_in]
thumbnail = args.shift
S3Object.url_for(full_filename(thumbnail), bucket_name, options)
end
def create_temp_file
write_to_temp_file current_data
end
def current_data
S3Object.value full_filename, bucket_name
end
def s3_protocol
Technoweenie::AttachmentFu::Backends::S3Backend.protocol
end
def s3_hostname
Technoweenie::AttachmentFu::Backends::S3Backend.hostname
end
def s3_port_string
Technoweenie::AttachmentFu::Backends::S3Backend.port_string
end
def cloudfront_distribution_domain
Technoweenie::AttachmentFu::Backends::S3Backend.distribution_domain
end
protected
# Called in the after_destroy callback
def destroy_file
S3Object.delete full_filename, bucket_name
end
def rename_file
return unless @old_filename && @old_filename != filename
old_full_filename = File.join(base_path, @old_filename)
S3Object.rename(
old_full_filename,
full_filename,
bucket_name,
:access => attachment_options[:s3_access]
)
@old_filename = nil
true
end
def save_to_storage
if save_attachment?
S3Object.store(
full_filename,
(temp_path ? File.open(temp_path) : temp_data),
bucket_name,
:content_type => content_type,
:access => attachment_options[:s3_access]
)
end
@old_filename = nil
true
end
end
end
end
end

View File

@ -0,0 +1,59 @@
require 'red_artisan/core_image/processor'
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
module Processors
module CoreImageProcessor
def self.included(base)
base.send :extend, ClassMethods
base.alias_method_chain :process_attachment, :processing
end
module ClassMethods
def with_image(file, &block)
block.call OSX::CIImage.from(file)
end
end
protected
def process_attachment_with_processing
return unless process_attachment_without_processing
with_image do |img|
self.width = img.extent.size.width if respond_to?(:width)
self.height = img.extent.size.height if respond_to?(:height)
resize_image_or_thumbnail! img
callback_with_args :after_resize, img
end if image?
end
# Performs the actual resizing operation for a thumbnail
def resize_image(img, size)
processor = ::RedArtisan::CoreImage::Processor.new(img)
size = size.first if size.is_a?(Array) && size.length == 1
if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
if size.is_a?(Fixnum)
processor.fit(size)
else
processor.resize(size[0], size[1])
end
else
new_size = [img.extent.size.width, img.extent.size.height] / size.to_s
processor.resize(new_size[0], new_size[1])
end
processor.render do |result|
self.width = result.extent.size.width if respond_to?(:width)
self.height = result.extent.size.height if respond_to?(:height)
# Get a new temp_path for the image before saving
temp_paths.unshift Tempfile.new(random_tempfile_filename, Technoweenie::AttachmentFu.tempfile_path).path
result.save self.temp_path, OSX::NSJPEGFileType
self.size = File.size(self.temp_path)
end
end
end
end
end
end

View File

@ -0,0 +1,54 @@
require 'rubygems'
require 'gd2'
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
module Processors
module Gd2Processor
def self.included(base)
base.send :extend, ClassMethods
base.alias_method_chain :process_attachment, :processing
end
module ClassMethods
# Yields a block containing a GD2 Image for the given binary data.
def with_image(file, &block)
im = GD2::Image.import(file)
block.call(im)
end
end
protected
def process_attachment_with_processing
return unless process_attachment_without_processing && image?
with_image do |img|
resize_image_or_thumbnail! img
self.width = img.width
self.height = img.height
callback_with_args :after_resize, img
end
end
# Performs the actual resizing operation for a thumbnail
def resize_image(img, size)
size = size.first if size.is_a?(Array) && size.length == 1
if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
if size.is_a?(Fixnum)
# Borrowed from image science's #thumbnail method and adapted
# for this.
scale = size.to_f / (img.width > img.height ? img.width.to_f : img.height.to_f)
img.resize!((img.width * scale).round(1), (img.height * scale).round(1), false)
else
img.resize!(size.first, size.last, false)
end
else
w, h = [img.width, img.height] / size.to_s
img.resize!(w, h, false)
end
temp_paths.unshift random_tempfile_filename
self.size = img.export(self.temp_path)
end
end
end
end
end

View File

@ -0,0 +1,61 @@
require 'image_science'
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
module Processors
module ImageScienceProcessor
def self.included(base)
base.send :extend, ClassMethods
base.alias_method_chain :process_attachment, :processing
end
module ClassMethods
# Yields a block containing an Image Science image for the given binary data.
def with_image(file, &block)
::ImageScience.with_image file, &block
end
end
protected
def process_attachment_with_processing
return unless process_attachment_without_processing && image?
with_image do |img|
self.width = img.width if respond_to?(:width)
self.height = img.height if respond_to?(:height)
resize_image_or_thumbnail! img
end
end
# Performs the actual resizing operation for a thumbnail
def resize_image(img, size)
# create a dummy temp file to write to
# ImageScience doesn't handle all gifs properly, so it converts them to
# pngs for thumbnails. It has something to do with trying to save gifs
# with a larger palette than 256 colors, which is all the gif format
# supports.
filename.sub! /gif$/, 'png'
content_type.sub!(/gif$/, 'png')
temp_paths.unshift write_to_temp_file(filename)
grab_dimensions = lambda do |img|
self.width = img.width if respond_to?(:width)
self.height = img.height if respond_to?(:height)
img.save self.temp_path
self.size = File.size(self.temp_path)
callback_with_args :after_resize, img
end
size = size.first if size.is_a?(Array) && size.length == 1
if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
if size.is_a?(Fixnum)
img.thumbnail(size, &grab_dimensions)
else
img.resize(size[0], size[1], &grab_dimensions)
end
else
new_size = [img.width, img.height] / size.to_s
img.resize(new_size[0], new_size[1], &grab_dimensions)
end
end
end
end
end
end

View File

@ -0,0 +1,132 @@
require 'mini_magick'
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
module Processors
module MiniMagickProcessor
def self.included(base)
base.send :extend, ClassMethods
base.alias_method_chain :process_attachment, :processing
end
module ClassMethods
# Yields a block containing an MiniMagick Image for the given binary data.
def with_image(file, &block)
begin
binary_data = file.is_a?(MiniMagick::Image) ? file : MiniMagick::Image.from_file(file) unless !Object.const_defined?(:MiniMagick)
rescue
# Log the failure to load the image.
logger.debug("Exception working with image: #{$!}")
binary_data = nil
end
block.call binary_data if block && binary_data
ensure
!binary_data.nil?
end
end
protected
def process_attachment_with_processing
return unless process_attachment_without_processing
with_image do |img|
resize_image_or_thumbnail! img
self.width = img[:width] if respond_to?(:width)
self.height = img[:height] if respond_to?(:height)
callback_with_args :after_resize, img
end if image?
end
# Performs the actual resizing operation for a thumbnail
def resize_image(img, size)
size = size.first if size.is_a?(Array) && size.length == 1
img.combine_options do |commands|
commands.strip unless attachment_options[:keep_profile]
# gif are not handled correct, this is a hack, but it seems to work.
if img.output =~ / GIF /
img.format("png")
end
if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
if size.is_a?(Fixnum)
size = [size, size]
commands.resize(size.join('x'))
else
commands.resize(size.join('x') + '!')
end
# extend to thumbnail size
elsif size.is_a?(String) and size =~ /e$/
size = size.gsub(/e/, '')
commands.resize(size.to_s + '>')
commands.background('#ffffff')
commands.gravity('center')
commands.extent(size)
# crop thumbnail, the smart way
elsif size.is_a?(String) and size =~ /c$/
size = size.gsub(/c/, '')
# calculate sizes and aspect ratio
thumb_width, thumb_height = size.split("x")
thumb_width = thumb_width.to_f
thumb_height = thumb_height.to_f
thumb_aspect = thumb_width.to_f / thumb_height.to_f
image_width, image_height = img[:width].to_f, img[:height].to_f
image_aspect = image_width / image_height
# only crop if image is not smaller in both dimensions
unless image_width < thumb_width and image_height < thumb_height
command = calculate_offset(image_width,image_height,image_aspect,thumb_width,thumb_height,thumb_aspect)
# crop image
commands.extract(command)
end
# don not resize if image is not as height or width then thumbnail
if image_width < thumb_width or image_height < thumb_height
commands.background('#ffffff')
commands.gravity('center')
commands.extent(size)
# resize image
else
commands.resize("#{size.to_s}")
end
# crop end
else
commands.resize(size.to_s)
end
end
temp_paths.unshift img
end
def calculate_offset(image_width,image_height,image_aspect,thumb_width,thumb_height,thumb_aspect)
# only crop if image is not smaller in both dimensions
# special cases, image smaller in one dimension then thumbsize
if image_width < thumb_width
offset = (image_height / 2) - (thumb_height / 2)
command = "#{image_width}x#{thumb_height}+0+#{offset}"
elsif image_height < thumb_height
offset = (image_width / 2) - (thumb_width / 2)
command = "#{thumb_width}x#{image_height}+#{offset}+0"
# normal thumbnail generation
# calculate height and offset y, width is fixed
elsif (image_aspect <= thumb_aspect or image_width < thumb_width) and image_height > thumb_height
height = image_width / thumb_aspect
offset = (image_height / 2) - (height / 2)
command = "#{image_width}x#{height}+0+#{offset}"
# calculate width and offset x, height is fixed
else
width = image_height * thumb_aspect
offset = (image_width / 2) - (width / 2)
command = "#{width}x#{image_height}+#{offset}+0"
end
# crop image
command
end
end
end
end
end

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