remove cruft, refactor

This commit is contained in:
Sami Samhuri 2012-02-15 19:26:56 -08:00
parent 7fc54e2450
commit 1185bc4354
78 changed files with 682 additions and 3937 deletions

View file

@ -3,22 +3,18 @@
A web server blueprint using Sinatra and Redis.
Storm Weather is only a vague idea. Trying to nail down a lightweight blueprint for
projects that have user accounts and admin accounts.
projects that have user accounts.
## Models
### Account Model
### Admin Model
## Controllers
### Account Controller
### Admin Controller
### Public Controller

13
lib/class-ext.rb Normal file
View file

@ -0,0 +1,13 @@
# Copyright 2012 Sami Samhuri <sami@samhuri.net>
class Class
def singleton
(class <<self; self end)
end
def define_class_method(name, &body)
singleton.send(:define_method, name, &body)
end
end

View file

@ -9,6 +9,10 @@ require 'active_support/core_ext'
this_dir = File.dirname(__FILE__)
$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
# Ruby extensions
require 'class-ext'
require 'hash-ext'
module Stormy
# key prefix for data stored in Redis (used for testing)
@ -16,12 +20,12 @@ module Stormy
KeyPrefix = ''
end
# public directory for project photos
# public directory for photos
unless const_defined? :PhotoDir
PhotoDir = File.expand_path('../public/photos', File.dirname(__FILE__))
end
# public directory for project videos
# public directory for videos
unless const_defined? :VideoDir
VideoDir = File.expand_path('../public/videos', File.dirname(__FILE__))
end

View file

@ -4,7 +4,7 @@ module Stormy
class Server < Sinatra::Base
get '/sign-up' do
redirect '/projects' if authorized? && production?
redirect '/account' if authorized? && production?
title 'Sign Up'
stylesheet 'sign-up'
@ -26,9 +26,9 @@ module Stormy
@account = Account.create(fields)
authorize_account(@account.id)
send_verification_mail(@account, 'Welcome to Stormy Weather!')
redirect '/projects'
redirect '/account'
rescue Account::EmailTakenError => e
rescue Account::DuplicateFieldError => e
flash[:warning] = "That email address is already taken."
session[:fields] = fields
session[:fields]['terms'] = params['terms']
@ -48,7 +48,7 @@ module Stormy
end
get '/sign-in' do
redirect '/projects' if authorized? && production?
redirect '/account' if authorized? && production?
title 'Sign In'
stylesheet 'sign-in'
@ -70,7 +70,7 @@ module Stormy
else
response.delete_cookie('remembered')
end
url = session.delete(:original_url) || '/projects'
url = session.delete(:original_url) || '/account'
redirect url
else
flash[:warning] = "Incorrect email address or password."
@ -121,7 +121,7 @@ module Stormy
authorize!
current_account.password = params['password']
current_account.save!
redirect '/projects'
redirect '/account'
end
get '/account' do
@ -186,7 +186,7 @@ module Stormy
current_account.update({ params['id'] => params['value'] })
end
ok
rescue Account::EmailTakenError => e
rescue Account::DuplicateFieldError => e
fail('taken')
rescue Account::InvalidDataError => e
fail('invalid')

View file

@ -6,48 +6,7 @@ module Stormy
get '/admin' do
admin_authorize!
title "Dashboard"
erb :'admin/dashboard', :layout => :'admin/layout'
end
get '/admin/sign-in' do
title "Sign In"
script 'sign-in'
stylesheet 'sign-in'
erb :'admin/sign-in', :layout => :'admin/layout'
end
post '/admin/sign-in' do
if id = Admin.check_password(params['email'], params['password'])
authorize_admin(id)
redirect session.delete(:original_url) || '/admin'
else
flash[:notice] = "Incorrect email address or password."
redirect '/admin/sign-in'
end
end
post '/admin/sign-out' do
session.delete(:admin_id)
redirect '/admin'
end
get '/admin/password' do
admin_authorize!
title 'Change password'
erb :'admin/password', :layout => :'admin/layout'
end
post '/admin/password' do
admin_authorize!
if params['password'] == params['password_confirmation']
current_admin.password = params['password']
current_admin.save
flash[:notice] = "Password changed."
redirect '/admin'
else
flash[:warning] = "Passwords do not match."
redirect '/admin/password'
end
erb :'admin/dashboard'
end
@ -60,7 +19,7 @@ module Stormy
mark_last_listing
title "Accounts"
@accounts = Account.fetch_all.sort { |a,b| a.name <=> b.name }
erb :'admin/accounts', :layout => :'admin/layout'
erb :'admin/accounts'
end
get '/admin/account/:email' do |email|
@ -69,7 +28,7 @@ module Stormy
mark_last_listing
title "#{@account.name}'s Account"
script 'admin-account'
erb :'admin/account', :layout => :'admin/layout'
erb :'admin/account'
else
flash[:notice] = "No account with email #{email}"
redirect last_listing
@ -79,7 +38,7 @@ module Stormy
get '/admin/sign-in-as/:email' do |email|
admin_authorize!
authorize_account(Account.id_from_email(email))
redirect '/projects'
redirect '/account'
end
get '/admin/account/:email/delete' do |email|
@ -105,7 +64,7 @@ module Stormy
if new_email != @account.email
begin
@account.update_email(new_email)
rescue Account::EmailTakenError => e
rescue Account::DuplicateFieldError => e
flash[:warning] = "That email address is already taken."
redirect '/admin/account/' + email
end
@ -124,38 +83,6 @@ module Stormy
end
################
### Projects ###
################
get '/admin/projects' do
admin_authorize!
mark_last_listing
title "Projects"
@projects = Project.fetch_all.sort { |a,b| a.id <=> b.id }
erb :'admin/projects', :layout => :'admin/layout'
end
get '/admin/project/:id' do |id|
admin_authorize!
if @project = Project.fetch(id)
title "Project ##{id}"
title "#{title} (#{@project.name})" if @project.name
script 'admin-project'
erb :'admin/project', :layout => :'admin/layout'
else
flash[:notice] = "No such project (ID #{id})."
redirect last_listing
end
end
get '/admin/project/:id/delete' do |id|
admin_authorize!
Project.delete!(id)
redirect last_listing
end
###########
### FAQ ###
###########
@ -170,7 +97,7 @@ module Stormy
<p class="answer">Yes my son.</p>
EOT
end
erb :'admin/faq', :layout => :'admin/layout'
erb :'admin/faq'
end
post '/admin/faq' do
@ -180,38 +107,5 @@ module Stormy
redirect '/admin/faq'
end
######################
### Admin Accounts ###
######################
get '/admin/admins' do
admin_authorize!
@admins = Admin.fetch_all.sort { |a,b| a.name <=> b.name }
@fields = session.delete(:fields) || {}
title 'Admin Accounts'
stylesheet 'admins'
erb :'admin/admins', :layout => :'admin/layout'
end
post '/admin/admins' do
admin_authorize!
if params['password'] == params['password_confirmation']
admin = Admin.create(params)
flash[:notice] = "Added #{params['name']} (#{params['email']}) as an admin."
else
session[:fields] = params.slice('name', 'email')
flash[:warning] = "Passwords do not match."
end
redirect '/admin/admins'
end
get '/admin/admins/:id/delete' do |id|
admin_authorize!
Admin.delete!(id)
flash[:notice] = "Deleted."
redirect '/admin/admins'
end
end
end

View file

@ -1,123 +0,0 @@
# Copyright 2011 Beta Street Media
module Stormy
class Server < Sinatra::Base
get '/projects' do
authorize!
@projects = current_account.sorted_projects
title 'Projects'
stylesheet 'projects'
script 'projects'
erb :projects
end
get '/project/:id' do |id|
authorize_project!(id)
if current_project.name.blank?
title "Project ID #{id}"
else
title current_project.name
end
stylesheet 'jquery.lightbox-0.5'
script 'jquery.lightbox-0.5'
script 'jquery.dragsort'
stylesheet 'edit-project'
script 'edit-project'
# fuck IE
if request.user_agent.match(/msie/i)
stylesheet 'uploadify'
script 'swfobject'
script 'jquery.uploadify.v2.1.4'
end
@errors = session.delete('errors')
@project = current_project
erb :'edit-project'
end
post '/project/update' do
id = params['id']
if admin_authorized?
current_project(id)
else
authorize_project!(id)
end
begin
current_project.update(params)
flash[:notice] = "Project saved."
rescue Project::InvalidDataError => e
flash[:warning] = "There are some errors with your project."
session['errors'] = e.fields
end
redirect '/project/' + params['id']
end
post '/project/add-photo' do
content_type :json
id = params['id']
if admin_authorized?
current_project(id)
else
authorize_project_api!(id)
end
if photo = current_project.add_photo(params['photo'][:tempfile].path)
ok({
'n' => current_project.count_photos,
'photo' => photo
})
else
fail('limit')
end
end
# fuck IE
post '/uploadify' do
content_type :json
authorize_project_api!(params['id'])
if photo = current_project.add_photo(params['Filedata'][:tempfile].path)
ok({
'n' => current_project.count_photos,
'photo' => photo
})
else
content_type 'text/plain'
bad_request
end
end
post '/project/remove-photo' do
content_type :json
if admin_authorized?
current_project(params['id'])
else
authorize_project_api!(params['id'])
end
current_project.remove_photo(params['photo_id'])
ok({
'photos' => current_project.photos
})
end
post '/project/photo-order' do
content_type :json
id = params['id']
if admin_authorized?
current_project(id)
else
authorize_project_api!(id)
end
current_project.photo_ids = params['order']
current_project.save!
ok
end
end
end

View file

@ -7,8 +7,6 @@ module Stormy
get '/' do
cache_control :public, :must_revalidate, :max_age => 60
stylesheet 'index'
stylesheet 'jquery.lightbox-0.5'
script 'jquery.lightbox-0.5'
script 'index'
erb :index
end

View file

@ -25,7 +25,6 @@ module Stormy
end
def send_verification_mail(account = current_account, subject = nil)
account.create_email_verification_token
body = erb(:'email/email-verification', :layout => :'email/layout', :locals => {
:name => account.first_name,
:email => account.email,

View file

@ -10,21 +10,13 @@ module Stormy
Account.count
end
def num_admins
Models::Admin.count
end
def num_projects
Project.count
end
# Used to redirect back to the most recent list of things.
#
# i.e. someone goes to /admin -> /admin/account/foo -> /admin/project/007
# if they delete that project they should go back to /admin/account/foo
# i.e. someone goes to /admin -> /admin/account/foo -> /admin/thing/007
# if they delete that thing they should go back to /admin/account/foo
#
# however if they go /admin -> /admin/projects -> /admin/project/007
# and then delete that project they should go back to /admin/projects
# however if they go /admin -> /admin/things -> /admin/thing/007
# and then delete that thing they should go back to /admin/things
def last_listing
session.delete(:last_listing) || '/admin'
end

View file

@ -42,53 +42,14 @@ module Stormy
end
end
def current_project(id = nil)
if id
@current_project = Project.fetch(id)
else
@current_project
end
end
def project_authorized?
current_project && current_account && current_project.account_id == current_account.id
end
def authorize_project_api!(id)
authorize_api!
current_project(id)
throw(:halt, fail('no such project')) unless current_project
unless project_authorized?
content_type 'text/plain'
throw(:halt, not_authorized)
end
end
def authorize_project!(id)
authorize!
current_project(id)
unless current_project && project_authorized?
flash[:warning] = 'No such project.'
redirect '/projects'
end
end
def authorize_admin(id)
session[:admin_id] = id
end
def deauthorize_admin
session.delete(:admin_id)
end
def admin_authorized?
session[:admin_id] && Models::Admin.exists?(session[:admin_id])
authorized? && current_account.has_role?('admin')
end
def admin_authorize!
unless admin_authorized?
session[:original_url] = request.url
redirect '/admin'
redirect '/sign-in'
end
end
@ -99,10 +60,6 @@ module Stormy
end
end
def current_admin
@current_admin ||= Models::Admin.fetch(session[:admin_id])
end
end
end
end

View file

@ -1,5 +1,6 @@
# Copyright 2012 Sami Samhuri <sami@samhuri.net>
require 'json'
require 'stormy/config'
module Stormy

View file

@ -64,6 +64,15 @@ module Stormy
end
end
def breadcrumbs
@breadcrumbs ||= []
end
def breadcrumb(crumb)
crumb[:path] ||= '/' + crumb[:name].downcase
breadcrumbs << crumb
end
def format_dollars(amount, currency = 'CAD')
'%s $%.2f' % [currency, amount / 100.0]
end
@ -116,6 +125,10 @@ module Stormy
RDiscount.new(s.to_s).to_html
end
def admin_page?(path = request.path_info)
path.starts_with?('/admin')
end
end
end
end

View file

@ -7,27 +7,26 @@ module Stormy
module Models
class Account < Base
class EmailTakenError < RuntimeError; end
class IncorrectPasswordError < RuntimeError; end
name 'account'
Roles = %w[user admin]
model_name 'account'
field :id, :required => true
field :email, :type => :email, :required => true
field :email, :type => :email, :required => true, :unique => true
field :first_name, :required => true, :updatable => true
field :last_name, :required => true, :updatable => true
field :phone, :type => :phone, :updatable => true
field :hashed_password, :required => true
field :password
field :created_timestamp, :type => :integer
field :email_verification_token, :nullify_if_blank => true
field :email_verified?
field :hashed_password, :required => true
field :password
field :password_reset_token, :nullify_if_blank => true
@@account_email_index_key = Stormy.key('index:account-email')
field :role, :required => true
### Class Methods
@ -41,18 +40,8 @@ module Stormy
end
end
def self.email_taken?(email)
!! redis.hget(@@account_email_index_key, email.to_s.strip.downcase)
end
def self.fetch_by_email(email)
if id = id_from_email(email)
fetch(id)
end
end
def self.reset_password(email)
if key = key_from_email(email)
if key = key(id_from_email(email))
token = redis.hget(key, 'password_reset_token')
if token.blank?
token = UUID.generate
@ -75,12 +64,8 @@ module Stormy
end
end
def self.id_from_email(email)
redis.hget(@@account_email_index_key, email.strip.downcase)
end
def self.verify_email(email, token)
if key = key_from_email(email)
if key = key(id_from_email(email))
expected_token = redis.hget(key, 'email_verification_token')
verified = token == expected_token
if verified
@ -92,21 +77,12 @@ module Stormy
end
def self.email_verified?(email)
if key = key_from_email(email)
if key = key(id_from_email(email))
redis.hget(key, 'email_verified') == 'true'
end
end
### Private Class Methods
def self.key_from_email(email)
key(id_from_email(email))
end
private_class_method :key_from_email
### Instance Methods
def initialize(fields = {}, options = {})
@ -120,36 +96,20 @@ module Stormy
end
def create
raise EmailTakenError if email_taken?
# new accounts get an id and timestamp
self.id = UUID.generate unless id.present?
self.created_timestamp = Time.now.to_i
self.role = 'user' if role.blank?
super
create_email_verification_token
# add to index
redis.hset(@@account_email_index_key, email.downcase, id)
self
end
def delete!
project_ids.each { |id| Project.delete!(id) }
super
redis.hdel(@@account_email_index_key, email.strip.downcase)
def has_role?(role)
Roles.index(self.role) >= Roles.index(role)
end
def email_taken?(email = @email)
self.class.email_taken?(email)
end
def create_email_verification_token
self.email_verification_token ||= UUID.generate
redis.hset(key, 'email_verification_token', email_verification_token)
email_verification_token
def email_verification_token
unless @email_verification_token
@email_verification_token = UUID.generate
redis.hset(key, 'email_verification_token', @email_verification_token)
end
@email_verification_token
end
def password
@ -167,41 +127,16 @@ module Stormy
"#{first_name} #{last_name}"
end
def count_projects
redis.scard(project_ids_key)
end
def project_ids
redis.smembers(project_ids_key)
end
def projects
project_ids.map { |pid| Project.fetch(pid) }
end
def sorted_projects
@sorted_projects ||= projects.sort { |a,b| a.created_timestamp <=> b.created_timestamp }
end
def add_project_id(id)
redis.sadd(project_ids_key, id)
end
def remove_project_id(id)
redis.srem(project_ids_key, id)
end
def update_email(new_email)
new_email = new_email.strip
if email != new_email
raise EmailTakenError if new_email.downcase != email.downcase && email_taken?(new_email)
raise InvalidDataError.new({ 'email' => 'invalid' }) unless field_valid?('email', new_email)
if email.downcase != new_email.downcase
self.email_verified = false
redis.hdel(@@account_email_index_key, email.downcase)
redis.hset(@@account_email_index_key, new_email.downcase, id)
end
changed = new_email.downcase != email.downcase
raise DuplicateFieldError.new(:email => new_email) if changed && email_taken?(new_email)
raise InvalidDataError.new('email' => 'invalid') unless field_valid?('email', new_email)
self.email_verified = false if changed
remove_from_field_index(:email) if changed
self.email = new_email
add_to_field_index(:email) if changed
save!
end
end
@ -214,13 +149,6 @@ module Stormy
self.password = new_password
end
private
def project_ids_key
@project_ids_key ||= "#{key}:project-ids"
end
end
end
end

View file

@ -1,124 +0,0 @@
# Copyright 2012 Sami Samhuri <sami@samhuri.net>
require 'bcrypt'
require 'uuid'
module Stormy
module Models
class Admin < Base
class EmailTakenError < RuntimeError; end
class IncorrectPasswordError < RuntimeError; end
name 'admin'
field :id, :required => true
field :name, :required => true, :updatable => true
field :email, :type => :email, :required => true
field :hashed_password, :required => true
field :password
@@admin_email_index_key = Stormy.key('index:admin-email')
### Class Methods
def self.key_from_email(email)
key(id_from_email(email))
end
def self.check_password(email, password)
id = id_from_email(email)
key = self.key(id)
if key
hashed_password = BCrypt::Password.new(redis.hget(key, 'hashed_password'))
id if hashed_password == password
end
end
def self.email_taken?(email)
!! redis.hget(@@admin_email_index_key, email.to_s.strip.downcase)
end
def self.fetch_by_email(email)
key = key_from_email(email)
new(redis.hgetall(key)) if key
end
def self.id_from_email(email)
redis.hget(@@admin_email_index_key, email.strip.downcase)
end
### Instance Methods
def initialize(fields = {}, options = {})
super(fields, options)
if fields['hashed_password']
self.hashed_password = BCrypt::Password.new(fields['hashed_password'])
else
self.password = fields['password']
end
end
def create
raise EmailTakenError if email_taken?
self.id = UUID.generate unless id.present?
super
# add to index
redis.hset(@@admin_email_index_key, @email.downcase, @id)
self
end
def delete!
if super
redis.hdel(@@admin_email_index_key, @email.strip.downcase)
end
end
def email_taken?(email = @email)
self.class.email_taken?(email)
end
def password
@password ||= BCrypt::Password.new(hashed_password)
end
def password=(new_password)
if new_password.present?
self.hashed_password = BCrypt::Password.create(new_password)
@password = nil
end
end
def update_email(new_email)
new_email = new_email.strip
if email != new_email
raise EmailTakenError if new_email.downcase != email.downcase && email_taken?(new_email)
raise InvalidDataError.new({ 'email' => 'invalid' }) unless field_valid?('email', new_email)
if email.downcase != new_email.downcase
redis.hdel(@@admin_email_index_key, email.downcase)
redis.hset(@@admin_email_index_key, new_email.downcase, id)
end
self.email = new_email
save!
end
end
def update_password(old_password, new_password)
hashed_password = BCrypt::Password.new(redis.hget(key, 'hashed_password'))
raise IncorrectPasswordError unless hashed_password == old_password
raise InvalidDataError.new({ 'password' => 'missing' }) if new_password.blank?
redis.hset(key, 'hashed_password', BCrypt::Password.create(new_password))
self.password = new_password
end
end
end
end

View file

@ -15,11 +15,18 @@ module Stormy
end
end
class DuplicateFieldError < RuntimeError
attr_reader :field
def initialize(field = nil)
@field = field
end
end
def self.clean_number(number)
number.gsub(/[^\d]/, '').sub(/^1/, '')
end
# Allows any 10 digit number in North America, or an empty field (for account creation).
# Allows any 10 digit number in North America, or an empty field
PhoneNumberValidator = proc do |number|
if number.present?
clean_number(number).length == 10
@ -48,7 +55,7 @@ module Stormy
# Define or retrieve the name of this model.
def self.name(name = nil)
def self.model_name(name = nil)
if name
@model_name = name
end
@ -61,20 +68,33 @@ module Stormy
@fields ||= {}
end
def self.inherit_from(parent)
fields.merge!(parent.fields)
parent.belongs_to_relationships.each do |key, value|
belongs_to_relationships[key] = value.dup
end
parent.has_many_relationships.each do |key, value|
has_many_relationships[key] = value.dup
end
end
# Define fields like so:
#
# field :id, :type => :integer, :required => true
# field :name, :required => true, :updatable => true
# field :name, :required => true, :updatable => true, :indexed => true
# field :email, :required => true, :updatable => true, :unique => true
# field :verified?
#
# Defaults: {
# :type => :string,
# :required => false,
# :updatable => false,
# :accessors => true,
# :validator => nil, # with some exceptions
# :default => {},
# :nullify_if_blank => false
# :nullify_if_blank => false,
# :indexed => false,
# :unique => false
# }
#
# Types: :string, :integer, :boolean, :json, as well as
@ -91,12 +111,26 @@ module Stormy
# JSON fields accept a :default option used to initialize
# a JSON field, and also when a parse fails.
#
# Attribute accessors are defined for each field and boolean
# fields get a predicate method as well, e.g. verified?
# Unless :accessors is false, attribute accessors are
# defined for each field. Boolean fields get a predicate
# method as well, e.g. verified?
#
# Changed fields are tracked and only changed fields are
# persisted on a `save`.
#
# If the :indexed option is truthy an index on that field
# will be created and maintained and there will be a class
# method to fetch objects by that field.
#
# e.g. fetch_by_name(name)
#
# If the :unique option is truthy then values for that field
# must be unique across all instances. This implies :indexed
# and adds a method to see if a value is taken, to the class
# and to instances.
#
# e.g. email_taken?(email)
#
def self.field(name, options = {})
if name.to_s.ends_with?('?')
options[:type] = :boolean
@ -106,79 +140,182 @@ module Stormy
name = name.to_sym
options[:type] ||= :string
unless options.has_key?(:accessors)
options[:accessors] = true
end
if options[:unique]
options[:indexed] = true
end
case options[:type]
when :email
options[:validator] ||= EmailAddressValidator
options[:validator] = EmailAddressValidator unless options.has_key?(:validator)
options[:type] = :string
when :phone
options[:validator] ||= PhoneNumberValidator
options[:validator] = PhoneNumberValidator unless options.has_key?(:validator)
options[:type] = :string
when :json
options[:default] ||= {}
options[:default] = {} unless options.has_key?(:default)
end
fields[name] = options
define_method(name) do
instance_variable_get("@#{name}")
end
case options[:type]
when :string
define_method("#{name}=") do |value|
s =
if options[:nullify_if_blank] && value.blank?
nil
else
value.to_s.strip
end
instance_variable_set("@#{name}", s)
changed_fields[name] = s
end
when :integer
define_method("#{name}=") do |value|
i = value.to_i
instance_variable_set("@#{name}", i)
changed_fields[name] = i
end
when :boolean
define_method("#{name}=") do |value|
b = value == 'true' || value == true
instance_variable_set("@#{name}", b)
changed_fields[name] = b
end
define_method("#{name}?") do
if options[:accessors]
define_method(name) do
instance_variable_get("@#{name}")
end
when :json
define_method(name) do
unless value = instance_variable_get("@#{name}")
value = options[:default].dup
send("#{name}=", value)
end
value
end
define_method("#{name}=") do |value|
obj =
if value.is_a?(String)
if value.length > 0
JSON.parse(value)
case options[:type]
when :string
define_method("#{name}=") do |value|
s =
if options[:nullify_if_blank] && value.blank?
nil
else
options[:default].dup
value.to_s.strip
end
else
value
instance_variable_set("@#{name}", s)
changed_fields[name] = s
end
when :integer
define_method("#{name}=") do |value|
i = value.to_i
instance_variable_set("@#{name}", i)
changed_fields[name] = i
end
when :boolean
define_method("#{name}=") do |value|
b = value == 'true' || value == true
instance_variable_set("@#{name}", b)
changed_fields[name] = b
end
define_method("#{name}?") do
instance_variable_get("@#{name}")
end
when :json
define_method(name) do
unless value = instance_variable_get("@#{name}")
value = options[:default].dup
send("#{name}=", value)
end
instance_variable_set("@#{name}", obj)
changed_fields[name] = obj
value
end
define_method("#{name}=") do |value|
obj =
if value.is_a?(String)
if value.length > 0
JSON.parse(value)
else
options[:default].dup
end
else
value
end
instance_variable_set("@#{name}", obj)
changed_fields[name] = obj
end
else
define_method("#{name}=") do |value|
instance_variable_set("@#{name}", value)
changed_fields[name] = value
end
end
end # if options[:accessors]
if options[:indexed]
index_key_method_name = "#{name}_index_key"
define_class_method(index_key_method_name) do
Stormy.key("index:#{model_name}-#{name}")
end
define_class_method("fetch_by_#{name}") do |value|
if id = send("id_from_#{name}", value)
fetch(id)
end
end
else
define_method("#{name}=") do |value|
instance_variable_set("@#{name}", value)
changed_fields[name] = value
define_class_method("id_from_#{name}") do |value|
redis.hget(send(index_key_method_name), value.to_s.strip.downcase)
end
end
if options[:unique]
define_class_method("#{name}_taken?") do |value|
!! send("id_from_#{name}", value)
end
define_method("#{name}_taken?") do |value|
self.class.send("#{name}_taken?", value)
end
end
end
#####################
### Relationships ###
#####################
def self.has_many_relationships
@has_many_relationships ||= {}
end
def self.has_many(things, options = {})
thing = things.to_s.singularize
options[:class_name] ||= thing.capitalize
has_many_relationships[thing] = options
define_method("#{thing}_ids_key") do
ivar_name = "@#{thing}_ids_key"
unless ids_key = instance_variable_get(ivar_name)
ids_key = "#{key}:#{thing}-ids"
instance_variable_set(ivar_name, ids_key)
end
ids_key
end
private "#{thing}_ids_key"
define_method("count_#{things}") do
redis.scard(send("#{thing}_ids_key"))
end
define_method("#{thing}_ids") do
redis.smembers(send("#{thing}_ids_key"))
end
define_method(things) do
klass = Stormy::Models.const_get(options[:class_name])
send("#{thing}_ids").map { |id| klass.fetch(id) }
end
define_method("add_#{thing}_id") do |id|
redis.sadd(send("#{thing}_ids_key"), id)
end
define_method("remove_#{thing}_id") do |id|
redis.srem(send("#{thing}_ids_key"), id)
end
end
def self.belongs_to_relationships
@belongs_to_relationships ||= {}
end
def self.belongs_to(thing, options = {})
options[:class_name] ||= thing.to_s.capitalize
field "#{thing}_id".to_sym, :required => options[:required]
belongs_to_relationships[thing] = options
define_method(thing) do
klass = Stormy::Models.const_get(options[:class_name])
if thing_id = send("#{thing}_id")
instance_variable_set("@#{thing}", klass.fetch(thing_id))
end
end
end
@ -246,14 +383,46 @@ module Stormy
end
def create
# check for unqiue fields
self.class.fields.each do |name, options|
if options[:unique] && send("#{name}_taken?", send(name))
raise DuplicateFieldError.new(name => send(name))
end
end
if has_field?(:id) && field_required?(:id)
self.id = UUID.generate unless id.present?
end
if has_field?(:created_timestamp)
self.created_timestamp = Time.now.to_i
end
# raises if invalid
save
add_to_index
add_to_indexes
self.class.belongs_to_relationships.each do |thing, options|
if obj = send(thing)
obj.send("add_#{self.class.model_name}_id", id)
end
end
self
end
def delete!
if redis.srem(self.class.model_ids_key, id)
self.class.has_many_relationships.each do |thing, options|
klass = Stormy::Models.const_get(options[:class_name])
send("#{thing}_ids").each { |id| klass.delete!(id) }
end
if remove_from_indexes
self.class.belongs_to_relationships.each do |thing, options|
if obj = send(thing)
obj.send("remove_#{self.class.model_name}_id", id)
end
end
redis.del(key)
end
end
@ -280,6 +449,12 @@ module Stormy
options[:validate] = true unless options.has_key?(:validate)
fields.each do |name, value|
if options[:all] || field_updatable?(name)
# ensure uniqueness
if options[:unique] && send("#{name}_taken?", value)
raise DuplicateFieldError.new(name => value)
end
send("#{name}=", value)
end
end
@ -296,6 +471,10 @@ module Stormy
end
def save!
if has_field?(:updated_timestamp)
self.updated_timestamp = Time.now.to_i
end
# always update JSON fields because they can be updated without our knowledge
field_names.each do |name|
if field_type(name) == :json
@ -317,6 +496,7 @@ module Stormy
end
def validate
# check for invalid fields
invalid_fields = field_names.inject({}) do |fields, name|
if field_validates?(name)
result = validate_field(name, send(name))
@ -329,6 +509,10 @@ module Stormy
end
end
def fields
Hash[field_names.zip(field_names.map { |name| send(name) })]
end
private
@ -336,8 +520,39 @@ module Stormy
@key ||= self.class.key(self.id)
end
def add_to_index
redis.sadd(self.class.model_ids_key, self.id)
def model_name
@model_name ||= self.class.model_name
end
def add_to_indexes
if redis.sadd(self.class.model_ids_key, id)
self.class.fields.each do |name, options|
add_to_field_index(name) if options[:indexed]
end
end
end
def add_to_field_index(name)
index_key = self.class.send("#{name}_index_key")
redis.hset(index_key, send(name).to_s.strip.downcase, id)
end
def remove_from_indexes
if redis.srem(self.class.model_ids_key, id)
success = true
self.class.fields.each do |name, options|
if options[:indexed]
success = success && remove_from_field_index(name)
end
break unless success
end
success
end
end
def remove_from_field_index(name)
index_key = self.class.send("#{name}_index_key")
redis.hdel(index_key, send(name).to_s.strip.downcase)
end
def changed_fields
@ -348,6 +563,10 @@ module Stormy
self.class.clean_number(number)
end
def has_field?(name)
self.class.fields.has_key?(name.to_sym)
end
def field_names
self.class.fields.keys
end
@ -360,6 +579,10 @@ module Stormy
self.class.fields[name.to_sym][:updatable]
end
def field_required?(name)
self.class.fields[name.to_sym][:required]
end
def validate_field(name, value)
valid = true
reason = nil

View file

@ -1,162 +0,0 @@
# Copyright 2012 Sami Samhuri <sami@samhuri.net>
require 'digest/sha1'
require 'fileutils'
require 'RMagick'
module Stormy
module Models
class Project < Base
# max width or height in pixels
MaxPhotoSize = 1200
MaxPhotos = 10
name 'project'
field :id, :required => true
field :name, :required => true, :updatable => true
field :account_id
field :created_timestamp, :type => :integer, :required => true
field :fizzled_timestamp, :type => :integer
field :funded_timestamp, :type => :integer
field :photo_ids, :type => :json, :default => []
@@project_name_index_key = Stormy.key('index:project-name')
def self.fetch_by_name(name)
if id = id_from_name(name)
fetch(id)
end
end
def self.id_from_name(name)
redis.hget(@@project_name_index_key, name.strip.downcase)
end
def create
self.id = UUID.generate unless id.present?
self.created_timestamp = Time.now.to_i
super
# add to index
redis.hset(@@project_name_index_key, name.downcase, id)
account.add_project_id(id) if account
self
end
def delete!
if super
remove_all_photos!
account.remove_project_id(id) if account
redis.hdel(@@project_name_index_key, name.strip.downcase)
end
end
def funded?
funded_timestamp > 0
end
def funded!
self.funded_timestamp = Time.now.to_i
save!
end
def fizzled?
fizzled_timestamp > 0
end
def fizzled!
self.fizzled_timestamp = Time.now.to_i
save!
end
def count_photos
photo_ids.length
end
def add_photo(path)
unless count_photos >= MaxPhotos
photo = Magick::Image.read(path).first
photo.auto_orient!
photo.change_geometry("#{MaxPhotoSize}x#{MaxPhotoSize}>") { |cols, rows, img| img.resize!(cols, rows) }
photo.format = 'jpg'
FileUtils.mkdir_p(photo_dir) unless File.exists?(photo_dir)
photo_id = Digest::SHA1.hexdigest(photo.to_blob)
photo.write(photo_path(photo_id)) { self.quality = 80 }
photo_ids << photo_id
save!
photo_data(photo_id)
end
end
def remove_photo(photo_id)
path = photo_path(photo_id)
if i = photo_ids.index(photo_id)
photo_ids.delete_at(i)
end
FileUtils.rm(path) if File.exists?(path) && !photo_ids.include?(photo_id)
save!
end
def photo_paths
photo_ids.map { |id| photo_path(id) }
end
def photo_urls
photo_ids.map { |photo_id| "/photos/#{id}/#{photo_id}.jpg" }
end
def photo_url(photo_id)
"/photos/#{id}/#{photo_id}.jpg"
end
def photo_data(photo_id)
{
'id' => photo_id,
'url' => photo_url(photo_id)
}
end
def photos
photo_ids.map { |id| photo_data(id) }
end
def account
if account_id
@account ||= Account.fetch(account_id)
end
end
private
def photo_dir
File.join(Stormy::PhotoDir, @id)
end
def photo_path(id)
File.join(photo_dir, "#{id}.jpg")
end
def photos_key
"#{key}:photos"
end
def remove_all_photos!
FileUtils.rm_rf(photo_dir) if File.exists?(photo_dir)
self.photo_ids = []
end
end
end
end

View file

@ -7,15 +7,11 @@ require 'sinatra/cookie_thief'
require 'sinatra/flash'
require 'erubis'
require 'json'
require 'pony'
require 'redis'
require 'redis-store'
require 'uuid'
# Ruby extensions
require 'hash-ext'
require 'stormy/models'
require 'stormy/controllers'
require 'stormy/helpers'
@ -74,8 +70,6 @@ module Stormy
if production?
body = erb(:'email/error-notification', :layout => false, :locals => {
:account => current_account,
:project => current_project,
:admin => current_admin,
:error => env['sinatra.error']
})
Pony.mail({

View file

@ -1,86 +0,0 @@
.section { width: 90% }
#photo-form
{ height: 0
; width: 0
; opacity: 0
; position: fixed
; top: 0
}
#upload-target
{ width: 0
; height: 0
; opacity: 0
}
.save
{ width: 90%
; margin: 1em auto
; text-align: right
}
input.save-button
{ background-color: #092
; border-color: #061
}
.save-button-spinner { display: none }
table#project-info
{ width: 90%
; margin: 1em auto
}
table#project-info th
{ color: #405e83
; text-align: right
; vertical-align: top
; padding: 0.2em 0.5em 0.7em
}
input[type="text"],
input[type="tel"],
input[type="url"]
{ padding: 0 0.2em
; border: #CCD3DB 1px solid
; font-size: 1em
; height: 1.5em
; margin: 0
}
textarea
{ border: #CCD3DB 1px solid
; font-size: 0.9em
; line-height: 1.2em
; padding: 0.3em
; margin: 0
}
#photos li
{ display: inline-block
; width: 72px
; height: 84px
; padding: 0
; margin: 0.2em 0.25em 0.7em
; text-align: center
}
li#add-photo-box { display: inline-block }
#photos li#add-photo-spinner { height: 18px }
.add-photo { font-size: 0.8em }
a.add-photo { font-size: 0.9em }
.remove-photo { color: #900 }
#ie-photo-uploaderQueue { display: none }
#ie-photo-uploaderUploader
{ height: 64px
; width: 64px
}
#drag-n-drop
{ padding-left: 3em
; font-style: italic
}

View file

@ -1,101 +0,0 @@
/**
* jQuery lightBox plugin
* This jQuery plugin was inspired and based on Lightbox 2 by Lokesh Dhakar (http://www.huddletogether.com/projects/lightbox2/)
* and adapted to me for use like a plugin from jQuery.
* @name jquery-lightbox-0.5.css
* @author Leandro Vieira Pinho - http://leandrovieira.com
* @version 0.5
* @date April 11, 2008
* @category jQuery plugin
* @copyright (c) 2008 Leandro Vieira Pinho (leandrovieira.com)
* @license CCAttribution-ShareAlike 2.5 Brazil - http://creativecommons.org/licenses/by-sa/2.5/br/deed.en_US
* @example Visit http://leandrovieira.com/projects/jquery/lightbox/ for more informations about this jQuery plugin
*/
#jquery-overlay {
position: absolute;
top: 0;
left: 0;
z-index: 90;
width: 100%;
height: 500px;
}
#jquery-lightbox {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 100;
text-align: center;
line-height: 0;
}
#jquery-lightbox a img { border: none; }
#lightbox-container-image-box {
position: relative;
background-color: #fff;
width: 250px;
height: 250px;
margin: 0 auto;
}
#lightbox-container-image { padding: 10px; }
#lightbox-loading {
position: absolute;
top: 40%;
left: 0%;
height: 25%;
width: 100%;
text-align: center;
line-height: 0;
}
#lightbox-nav {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 10;
}
#lightbox-container-image-box > #lightbox-nav { left: 0; }
#lightbox-nav a { outline: none;}
#lightbox-nav-btnPrev, #lightbox-nav-btnNext {
width: 49%;
height: 100%;
zoom: 1;
display: block;
}
#lightbox-nav-btnPrev {
left: 0;
float: left;
}
#lightbox-nav-btnNext {
right: 0;
float: right;
}
#lightbox-container-image-data-box {
font: 10px Verdana, Helvetica, sans-serif;
background-color: #fff;
margin: 0 auto;
line-height: 1.4em;
overflow: auto;
width: 100%;
padding: 0 10px 0;
}
#lightbox-container-image-data {
padding: 0 10px;
color: #666;
}
#lightbox-container-image-data #lightbox-image-details {
width: 70%;
float: left;
text-align: left;
}
#lightbox-image-details-caption { font-weight: bold; }
#lightbox-image-details-currentNumber {
display: block;
clear: left;
padding-bottom: 1.0em;
}
#lightbox-secNav-btnClose {
width: 66px;
float: right;
padding-bottom: 0.7em;
}

View file

@ -1,35 +0,0 @@
table#projects
{ border-collapse: collapse
; margin: 0 auto 2em
}
#projects td { padding: 0.3em 0 }
#projects th { text-align: center }
#projects tr.headings th
{ color: #666
; padding: 0.3em 0 0.8em
}
#projects th.name,
#projects td.name
{ text-align: left }
#projects tr.project { background-color: #dce1e8 }
#projects tr.project:nth-child(even) { background-color: #e4e8ed }
#projects tr.project:hover { background-color: #eee }
#projects tr.project td
{ border-bottom: solid 1px #aaa
; padding: 0.5em 0
}
#projects tr.project.first td { border-top: solid 1px #aaa }
#projects td.created { text-align: center }
#projects td.name a
{ color: #333
; font-size: 1.2em
}

View file

@ -1,52 +0,0 @@
/*
Uploadify v2.1.4
Release Date: November 8, 2010
Copyright (c) 2010 Ronnie Garcia, Travis Nickels
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.
*/
.uploadifyQueueItem {
background-color: #F5F5F5;
border: 2px solid #E5E5E5;
font: 11px Verdana, Geneva, sans-serif;
margin-top: 5px;
padding: 10px;
width: 350px;
}
.uploadifyError {
background-color: #FDE5DD !important;
border: 2px solid #FBCBBC !important;
}
.uploadifyQueueItem .cancel {
float: right;
}
.uploadifyQueue .completed {
background-color: #E5E5E5;
}
.uploadifyProgress {
background-color: #E5E5E5;
margin-top: 10px;
width: 100%;
}
.uploadifyProgressBar {
background-color: #0099FF;
height: 3px;
width: 1px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -1,9 +1,9 @@
$(function() {
$('#change-password-link').click(function() {
$(this).hide()
$('#change-password').show()
$('#password-changed').hide()
$(this).addClass('hidden')
$('#change-password').removeClass('hidden')
$('#password-changed').addClass('hidden')
$('#old-password').focus()
return false
})
@ -14,8 +14,8 @@ $(function() {
})
$('#send-email-verification').click(function() {
$('#sending-email-verification').show()
$(this).hide()
$('#sending-email-verification').removeClass('hidden')
$(this).addClass('hidden')
var self = this
$.post('/account/send-email-verification', function(data) {
if (data.status === 'ok') {
@ -29,8 +29,8 @@ $(function() {
}).error(function() {
alert('Failed to send verification email. Try again later.')
}).complete(function() {
$('#sending-email-verification').hide()
$(self).show()
$('#sending-email-verification').addClass('hidden')
$(self).removeClass('hidden')
})
return false
})
@ -43,14 +43,14 @@ function changePassword() {
, confirmation = $('#password-confirmation').val()
if ($.trim(oldPassword) && $.trim(newPassword) && newPassword === confirmation) {
$('#change-password-form input[type="password"]').removeClass('error')
$('#change-password-form input[type="submit"]').hide()
$('#change-password-form .spinner').show()
$('#change-password-form input[type="submit"]').addClass('hidden')
$('#change-password-spinner').removeClass('hidden')
$.post('/account/password', $('#change-password-form').serialize(), function(data) {
if (data.status === 'ok') {
$('input[type="password"]').val('')
$('#change-password').hide()
$('#change-password-link').show()
$('#password-changed').show()
$('#change-password').addClass('hidden')
$('#change-password-link').removeClass('hidden')
$('#password-changed').removeClass('hidden')
}
// incorrect old password
else if (data.reason === 'incorrect') {
@ -72,8 +72,8 @@ function changePassword() {
}).error(function(x) {
alert('Failed to change password. Try again later.')
}).complete(function() {
$('#change-password-form input[type="submit"]').show()
$('#change-password-form .spinner').hide()
$('#change-password-form input[type="submit"]').removeClass('hidden')
$('#change-password-spinner').addClass('hidden')
})
}
else {

View file

@ -1,7 +0,0 @@
$(function() {
$('#delete').click(function() {
return confirm("Are you sure?")
})
})

View file

@ -1,215 +0,0 @@
$(function() {
initTmpl()
$('#name').blur(function() {
var name = $.trim($(this).val())
if (name === this.placeholder) name = ''
$('.save-button').val('Save ' + (name || 'This Project'))
})
$('form#project').submit(validateProject)
initLightBox()
$('#photos').dragsort({
dragSelector: 'li.photo'
, itemSelector: 'li.photo'
, dragEnd: updatePhotoOrder
, placeHolderTemplate: '<li class="placeholder"><div></div></li>'
, itemClicked: function(item) { $('a.thumbnail', item).click() }
})
var $photos = $('#photos-container')
, $addPhotoBox = $('#add-photo-box')
, $photoUploader = $('#photo-uploader')
// fuck IE
if ($.browser.msie) {
$('#ie-photo-uploader').uploadify({
'uploader' : '/uploadify/uploadify.swf',
'script' : '/uploadify',
'multi' : false,
'buttonImg' : '/images/add-photo.png',
'method' : 'post',
'cancelImg' : '/uploadify/cancel.png',
'auto' : true,
'fileExt' : ['jpg', 'jpeg', 'png'],
'sizeLimit' : 10 * 1024 * 1024 * 1024, // 10 MB is way more than enough
'scriptData' : { id: window.SI.projectId },
'onComplete': function(_a, _b, _c, text) {
completePhotoUpload(text)
},
'onError': function() {
completePhotoUpload('fail')
},
'onOpen': function() {
$('#add-photo-spinner').remove()
$addPhotoBox.addClass('hidden').before('<li id="add-photo-spinner"><img src="/images/spinner.gif"></li>')
}
})
}
$('.add-photo').click(function() {
$photoUploader.focus().click()
return false
})
$photoUploader.change(function() {
$addPhotoBox.addClass('hidden').before('<li id="add-photo-spinner"><img src="/images/spinner.gif"></li>')
$('#photo-form').submit()
return false
})
$('#upload-target').load(function() {
completePhotoUpload($(this).contents().text())
})
var photoTemplate = window.SI.tmpl($('#photo-template').html())
function completePhotoUpload(text) {
$('#add-photo-spinner').remove()
var photoForm = $('#photo-form').get(0)
if (photoForm) photoForm.reset()
try {
var response = JSON.parse(text)
if (response.status === 'ok') {
$addPhotoBox.before(photoTemplate(response.data.photo))
initLightBox()
if (response.data.n >= 10) {
$addPhotoBox.addClass('hidden')
}
else {
$addPhotoBox.removeClass('hidden')
}
}
else {
$addPhotoBox.removeClass('hidden')
alert('Failed to add photo. Try again later.')
}
}
catch (e) {
$addPhotoBox.removeClass('hidden')
alert('Failed to add photo. Try again later.')
}
}
var removeCount = 0
$('.remove-photo').live('click', function() {
var id = this.id
, photoId = id.replace(/^remove-photo-/, '')
, data = { id: window.SI.projectId, photo_id: photoId }
, spinnerId = 'remove-photo-spinner-' + photoId
, $this = $(this)
$this.hide().after('<img id="' + spinnerId + '" src="/images/spinner.gif">')
removeCount += 1
$.post('/project/remove-photo', data, function(response) {
removeCount -= 1
if (response.status === 'ok' && removeCount === 0) {
$addPhotoBox.removeClass('hidden')
$('li.photo').remove()
$.each(response.data.photos, function(i, photo) {
$addPhotoBox.before(photoTemplate(photo))
})
initLightBox()
}
else {
if (removeCount === 0) {
$('#' + spinnerId).remove()
$this.show()
alert('Failed to remove photo. Try again later.')
}
}
}).error(function() {
removeCount -= 1
if (removeCount === 0) {
$('#' + spinnerId).remove()
$this.show()
alert('Failed to remove photo. Try again later.')
}
})
return false
})
})
function initLightBox() {
$('#photos a.thumbnail').lightBox()
$('#photos a.thumbnail').live('click', function(){ console.dir(this) })
}
// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
// http://ejohn.org/blog/javascript-micro-templating/
function initTmpl() {
var cache = {}
window.SI = window.SI || {}
window.SI.tmpl = function tmpl(str, data) {
// Figure out if we're getting a template, or if we need to
// load the template - and be sure to cache the result.
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl($('#' + str).html()) :
// Generate a reusable function that will serve as a template
// generator (and which will be cached).
new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments)};" +
// Introduce the data as local variables using with(){}
"with(obj){p.push('" +
// Convert the template into pure JavaScript
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'")
+ "')}return p.join('')")
// Provide some basic currying to the user
return data ? fn( data ) : fn
}
}
function updatePhotoOrder() {
initLightBox()
var ids = []
$('#photos li.photo').each(function() {
ids.push(this.id.replace('photo-', ''))
})
var data = { id: window.SI.projectId, order: ids }
$.post('/project/photo-order', data, function(response) {
// noop
}).error(function() {
alert('Failed to reorder photos. Try again later.')
})
}
function validateProject() {
var valid = true
, nameField = $('#name')
if ($.trim(nameField.val()).length === 0) {
valid = false
nameField.addClass('error').focus().select()
}
else {
nameField.removeClass('error')
}
if (valid) {
$('.save-button').hide()
$('.save-button-spinner').show()
}
return valid
}

View file

@ -1,297 +0,0 @@
// jQuery List DragSort v0.4
// Website: http://dragsort.codeplex.com/
// License: http://dragsort.codeplex.com/license
(function($) {
$.fn.dragsort = function(options) {
var opts = $.extend({}, $.fn.dragsort.defaults, options);
var lists = [];
var list = null, lastPos = null;
this.each(function(i, cont) {
if ($(cont).is("table") && $(cont).children().size() == 1 && $(cont).children().is("tbody"))
cont = $(cont).children().get(0);
var newList = {
draggedItem: null,
placeHolderItem: null,
pos: null,
offset: null,
offsetLimit: null,
scroll: null,
container: cont,
init: function() {
$(this.container).attr("data-listIdx", i).mousedown(this.grabItem)
$(this.container).children(opts.itemSelector).each(function(j) { $(this).attr("data-itemIdx", j); });
},
grabItem: function(e) {
if (e.which != 1 || $(e.target).is(opts.dragSelectorExclude))
return;
var elm = e.target;
while (!$(elm).is("[data-listIdx='" + $(this).attr("data-listIdx") + "'] " + opts.dragSelector)) {
if (elm == this) return;
elm = elm.parentNode;
}
if (list != null && list.draggedItem != null)
list.dropItem();
list = lists[$(this).attr("data-listIdx")];
list.draggedItem = $(elm).closest(opts.itemSelector);
var mt = parseInt(list.draggedItem.css("marginTop"));
var ml = parseInt(list.draggedItem.css("marginLeft"));
list.offset = list.draggedItem.offset();
list.offset.top = e.pageY - list.offset.top + (isNaN(mt) ? 0 : mt) - 1;
list.offset.left = e.pageX - list.offset.left + (isNaN(ml) ? 0 : ml) - 1;
list.draggedItem.startOffset = list.draggedItem.offset()
list.draggedItem.startTime = +new Date()
if (!opts.dragBetween) {
var containerHeight = $(list.container).outerHeight() == 0 ? Math.max(1, Math.round(0.5 + $(list.container).children(opts.itemSelector).size() * list.draggedItem.outerWidth() / $(list.container).outerWidth())) * list.draggedItem.outerHeight() : $(list.container).outerHeight();
list.offsetLimit = $(list.container).offset();
list.offsetLimit.right = list.offsetLimit.left + $(list.container).outerWidth() - list.draggedItem.outerWidth();
list.offsetLimit.bottom = list.offsetLimit.top + containerHeight - list.draggedItem.outerHeight();
}
var h = list.draggedItem.height();
var w = list.draggedItem.width();
var orig = list.draggedItem.attr("style");
list.draggedItem.attr("data-origStyle", orig ? orig : "");
if (opts.itemSelector == "tr") {
list.draggedItem.children().each(function() { $(this).width($(this).width()); });
list.placeHolderItem = list.draggedItem.clone().attr("data-placeHolder", true);
list.draggedItem.after(list.placeHolderItem);
list.placeHolderItem.children().each(function() { $(this).css({ borderWidth:0, width: $(this).width() + 1, height: $(this).height() + 1 }).html("&nbsp;"); });
} else {
list.draggedItem.after(opts.placeHolderTemplate);
list.placeHolderItem = list.draggedItem.next().css({ height: h, width: w }).attr("data-placeHolder", true);
}
list.draggedItem.css({ position: "absolute", opacity: 0.8, "z-index": 999, height: h, width: w });
$(lists).each(function(i, l) { l.createDropTargets(); l.buildPositionTable(); });
list.scroll = { moveX: 0, moveY: 0, maxX: $(document).width() - $(window).width(), maxY: $(document).height() - $(window).height() };
list.scroll.scrollY = window.setInterval(function() {
if (opts.scrollContainer != window) {
$(opts.scrollContainer).scrollTop($(opts.scrollContainer).scrollTop() + list.scroll.moveY);
return;
}
var t = $(opts.scrollContainer).scrollTop();
if (list.scroll.moveY > 0 && t < list.scroll.maxY || list.scroll.moveY < 0 && t > 0) {
$(opts.scrollContainer).scrollTop(t + list.scroll.moveY);
list.draggedItem.css("top", list.draggedItem.offset().top + list.scroll.moveY + 1);
}
}, 10);
list.scroll.scrollX = window.setInterval(function() {
if (opts.scrollContainer != window) {
$(opts.scrollContainer).scrollLeft($(opts.scrollContainer).scrollLeft() + list.scroll.moveX);
return;
}
var l = $(opts.scrollContainer).scrollLeft();
if (list.scroll.moveX > 0 && l < list.scroll.maxX || list.scroll.moveX < 0 && l > 0) {
$(opts.scrollContainer).scrollLeft(l + list.scroll.moveX);
list.draggedItem.css("left", list.draggedItem.offset().left + list.scroll.moveX + 1);
}
}, 10);
list.setPos(e.pageX, e.pageY);
$(document).bind("selectstart", list.stopBubble); //stop ie text selection
$(document).bind("mousemove", list.swapItems);
$(document).bind("mouseup", list.dropItem);
if (opts.scrollContainer != window)
$(window).bind("DOMMouseScroll mousewheel", list.wheel);
return false; //stop moz text selection
},
setPos: function(x, y) {
var top = y - this.offset.top;
var left = x - this.offset.left;
if (!opts.dragBetween) {
top = Math.min(this.offsetLimit.bottom, Math.max(top, this.offsetLimit.top));
left = Math.min(this.offsetLimit.right, Math.max(left, this.offsetLimit.left));
}
this.draggedItem.parents().each(function() {
if ($(this).css("position") != "static" && (!$.browser.mozilla || $(this).css("display") != "table")) {
var offset = $(this).offset();
top -= offset.top;
left -= offset.left;
return false;
}
});
if (opts.scrollContainer == window) {
y -= $(window).scrollTop();
x -= $(window).scrollLeft();
y = Math.max(0, y - $(window).height() + 5) + Math.min(0, y - 5);
x = Math.max(0, x - $(window).width() + 5) + Math.min(0, x - 5);
} else {
var cont = $(opts.scrollContainer);
var offset = cont.offset();
y = Math.max(0, y - cont.height() - offset.top) + Math.min(0, y - offset.top);
x = Math.max(0, x - cont.width() - offset.left) + Math.min(0, x - offset.left);
}
list.scroll.moveX = x == 0 ? 0 : x * opts.scrollSpeed / Math.abs(x);
list.scroll.moveY = y == 0 ? 0 : y * opts.scrollSpeed / Math.abs(y);
this.draggedItem.css({ top: top, left: left });
},
wheel: function(e) {
if (($.browser.safari || $.browser.mozilla) && list && opts.scrollContainer != window) {
var cont = $(opts.scrollContainer);
var offset = cont.offset();
if (e.pageX > offset.left && e.pageX < offset.left + cont.width() && e.pageY > offset.top && e.pageY < offset.top + cont.height()) {
var delta = e.detail ? e.detail * 5 : e.wheelDelta / -2;
cont.scrollTop(cont.scrollTop() + delta);
e.preventDefault();
}
}
},
buildPositionTable: function() {
var item = this.draggedItem == null ? null : this.draggedItem.get(0);
var pos = [];
$(this.container).children(opts.itemSelector).each(function(i, elm) {
if (elm != item) {
var loc = $(elm).offset();
loc.right = loc.left + $(elm).width();
loc.bottom = loc.top + $(elm).height();
loc.elm = elm;
pos.push(loc);
}
});
this.pos = pos;
},
dropItem: function() {
if (list.draggedItem == null)
return;
list.placeHolderItem.before(list.draggedItem);
//list.draggedItem.attr("style", "") doesn't work on IE8 and jQuery 1.5 or lower
//list.draggedItem.removeAttr("style") doesn't work on chrome and jQuery 1.6 (works jQuery 1.5 or lower)
var orig = list.draggedItem.attr("data-origStyle");
list.draggedItem.attr("style", orig);
if (orig == "")
list.draggedItem.removeAttr("style");
list.draggedItem.removeAttr("data-origStyle");
list.placeHolderItem.remove();
$("[data-dropTarget]").remove();
window.clearInterval(list.scroll.scrollY);
window.clearInterval(list.scroll.scrollX);
var changed = false;
$(lists).each(function() {
$(this.container).children(opts.itemSelector).each(function(j) {
if (parseInt($(this).attr("data-itemIdx")) != j) {
changed = true;
$(this).attr("data-itemIdx", j);
}
});
});
var duration = +new Date() - list.draggedItem.startTime
, offset = list.draggedItem.offset()
, diffY = Math.abs(list.draggedItem.startOffset.top - offset.top)
, diffX = Math.abs(list.draggedItem.startOffset.left - offset.left)
, itemClicked = duration < 500 && diffX < 3 && diffY < 3
if (changed)
opts.dragEnd.apply(list.draggedItem);
else if (itemClicked)
opts.itemClicked(list.draggedItem)
list.draggedItem = null;
$(document).unbind("selectstart", list.stopBubble);
$(document).unbind("mousemove", list.swapItems);
$(document).unbind("mouseup", list.dropItem);
if (opts.scrollContainer != window)
$(window).unbind("DOMMouseScroll mousewheel", list.wheel);
return false;
},
stopBubble: function() { return false; },
swapItems: function(e) {
if (list.draggedItem == null)
return false;
list.setPos(e.pageX, e.pageY);
var ei = list.findPos(e.pageX, e.pageY);
var nlist = list;
for (var i = 0; ei == -1 && opts.dragBetween && i < lists.length; i++) {
ei = lists[i].findPos(e.pageX, e.pageY);
nlist = lists[i];
}
if (ei == -1 || $(nlist.pos[ei].elm).attr("data-placeHolder"))
return false;
if (lastPos == null || lastPos.top > list.draggedItem.offset().top || lastPos.left > list.draggedItem.offset().left)
$(nlist.pos[ei].elm).before(list.placeHolderItem);
else
$(nlist.pos[ei].elm).after(list.placeHolderItem);
$(lists).each(function(i, l) { l.createDropTargets(); l.buildPositionTable(); });
lastPos = list.draggedItem.offset();
return false;
},
findPos: function(x, y) {
for (var i = 0; i < this.pos.length; i++) {
if (this.pos[i].left < x && this.pos[i].right > x && this.pos[i].top < y && this.pos[i].bottom > y)
return i;
}
return -1;
},
createDropTargets: function() {
if (!opts.dragBetween)
return;
$(lists).each(function() {
var ph = $(this.container).find("[data-placeHolder]");
var dt = $(this.container).find("[data-dropTarget]");
if (ph.size() > 0 && dt.size() > 0)
dt.remove();
else if (ph.size() == 0 && dt.size() == 0) {
//list.placeHolderItem.clone().removeAttr("data-placeHolder") crashes in IE7 and jquery 1.5.1 (doesn't in jquery 1.4.2 or IE8)
$(this.container).append(list.placeHolderItem.removeAttr("data-placeHolder").clone().attr("data-dropTarget", true));
list.placeHolderItem.attr("data-placeHolder", true);
}
});
}
};
newList.init();
lists.push(newList);
});
return this;
};
$.fn.dragsort.defaults = {
itemClicked: function() { },
itemSelector: "li",
dragSelector: "li",
dragSelectorExclude: "input, textarea, a[href]",
dragEnd: function() { },
dragBetween: false,
placeHolderTemplate: "<li>&nbsp;</li>",
scrollContainer: window,
scrollSpeed: 5
};
})(jQuery);

View file

@ -1,472 +0,0 @@
/**
* jQuery lightBox plugin
* This jQuery plugin was inspired and based on Lightbox 2 by Lokesh Dhakar (http://www.huddletogether.com/projects/lightbox2/)
* and adapted to me for use like a plugin from jQuery.
* @name jquery-lightbox-0.5.js
* @author Leandro Vieira Pinho - http://leandrovieira.com
* @version 0.5
* @date April 11, 2008
* @category jQuery plugin
* @copyright (c) 2008 Leandro Vieira Pinho (leandrovieira.com)
* @license CCAttribution-ShareAlike 2.5 Brazil - http://creativecommons.org/licenses/by-sa/2.5/br/deed.en_US
* @example Visit http://leandrovieira.com/projects/jquery/lightbox/ for more informations about this jQuery plugin
*/
// Offering a Custom Alias suport - More info: http://docs.jquery.com/Plugins/Authoring#Custom_Alias
(function($) {
/**
* $ is an alias to jQuery object
*
*/
$.fn.lightBox = function(settings) {
// Settings to configure the jQuery lightBox plugin how you like
settings = jQuery.extend({
// Configuration related to overlay
overlayBgColor: '#000', // (string) Background color to overlay; inform a hexadecimal value like: #RRGGBB. Where RR, GG, and BB are the hexadecimal values for the red, green, and blue values of the color.
overlayOpacity: 0.8, // (integer) Opacity value to overlay; inform: 0.X. Where X are number from 0 to 9
// Configuration related to navigation
fixedNavigation: false, // (boolean) Boolean that informs if the navigation (next and prev button) will be fixed or not in the interface.
// Configuration related to images
imageLoading: '/images/lightbox-ico-loading.gif', // (string) Path and the name of the loading icon
imageBtnPrev: '/images/lightbox-btn-prev.gif', // (string) Path and the name of the prev button image
imageBtnNext: '/images/lightbox-btn-next.gif', // (string) Path and the name of the next button image
imageBtnClose: '/images/lightbox-btn-close.gif', // (string) Path and the name of the close btn
imageBlank: '/images/lightbox-blank.gif', // (string) Path and the name of a blank image (one pixel)
// Configuration related to container image box
containerBorderSize: 10, // (integer) If you adjust the padding in the CSS for the container, #lightbox-container-image-box, you will need to update this value
containerResizeSpeed: 400, // (integer) Specify the resize duration of container image. These number are miliseconds. 400 is default.
// Configuration related to texts in caption. For example: Image 2 of 8. You can alter either "Image" and "of" texts.
txtImage: 'Image', // (string) Specify text "Image"
txtOf: 'of', // (string) Specify text "of"
// Configuration related to keyboard navigation
keyToClose: 'c', // (string) (c = close) Letter to close the jQuery lightBox interface. Beyond this letter, the letter X and the SCAPE key is used to.
keyToPrev: 'p', // (string) (p = previous) Letter to show the previous image
keyToNext: 'n', // (string) (n = next) Letter to show the next image.
// Don´t alter these variables in any way
imageArray: [],
activeImage: 0
},settings);
// Caching the jQuery object with all elements matched
var jQueryMatchedObj = this; // This, in this context, refer to jQuery object
/**
* Initializing the plugin calling the start function
*
* @return boolean false
*/
function _initialize() {
_start(this,jQueryMatchedObj); // This, in this context, refer to object (link) which the user have clicked
return false; // Avoid the browser following the link
}
/**
* Start the jQuery lightBox plugin
*
* @param object objClicked The object (link) whick the user have clicked
* @param object jQueryMatchedObj The jQuery object with all elements matched
*/
function _start(objClicked,jQueryMatchedObj) {
// Hime some elements to avoid conflict with overlay in IE. These elements appear above the overlay.
$('embed, object, select').css({ 'visibility' : 'hidden' });
// Call the function to create the markup structure; style some elements; assign events in some elements.
_set_interface();
// Unset total images in imageArray
settings.imageArray.length = 0;
// Unset image active information
settings.activeImage = 0;
// We have an image set? Or just an image? Let´s see it.
if ( jQueryMatchedObj.length == 1 ) {
settings.imageArray.push(new Array(objClicked.getAttribute('href'),objClicked.getAttribute('title')));
} else {
// Add an Array (as many as we have), with href and title atributes, inside the Array that storage the images references
for ( var i = 0; i < jQueryMatchedObj.length; i++ ) {
settings.imageArray.push(new Array(jQueryMatchedObj[i].getAttribute('href'),jQueryMatchedObj[i].getAttribute('title')));
}
}
while ( settings.imageArray[settings.activeImage][0] != objClicked.getAttribute('href') ) {
settings.activeImage++;
}
// Call the function that prepares image exibition
_set_image_to_view();
}
/**
* Create the jQuery lightBox plugin interface
*
* The HTML markup will be like that:
<div id="jquery-overlay"></div>
<div id="jquery-lightbox">
<div id="lightbox-container-image-box">
<div id="lightbox-container-image">
<img src="../fotos/XX.jpg" id="lightbox-image">
<div id="lightbox-nav">
<a href="#" id="lightbox-nav-btnPrev"></a>
<a href="#" id="lightbox-nav-btnNext"></a>
</div>
<div id="lightbox-loading">
<a href="#" id="lightbox-loading-link">
<img src="/images/lightbox-ico-loading.gif">
</a>
</div>
</div>
</div>
<div id="lightbox-container-image-data-box">
<div id="lightbox-container-image-data">
<div id="lightbox-image-details">
<span id="lightbox-image-details-caption"></span>
<span id="lightbox-image-details-currentNumber"></span>
</div>
<div id="lightbox-secNav">
<a href="#" id="lightbox-secNav-btnClose">
<img src="/images/lightbox-btn-close.gif">
</a>
</div>
</div>
</div>
</div>
*
*/
function _set_interface() {
// Apply the HTML markup into body tag
$('body').append('<div id="jquery-overlay"></div><div id="jquery-lightbox"><div id="lightbox-container-image-box"><div id="lightbox-container-image"><img id="lightbox-image"><div style="" id="lightbox-nav"><a href="#" id="lightbox-nav-btnPrev"></a><a href="#" id="lightbox-nav-btnNext"></a></div><div id="lightbox-loading"><a href="#" id="lightbox-loading-link"><img src="' + settings.imageLoading + '"></a></div></div></div><div id="lightbox-container-image-data-box"><div id="lightbox-container-image-data"><div id="lightbox-image-details"><span id="lightbox-image-details-caption"></span><span id="lightbox-image-details-currentNumber"></span></div><div id="lightbox-secNav"><a href="#" id="lightbox-secNav-btnClose"><img src="' + settings.imageBtnClose + '"></a></div></div></div></div>');
// Get page sizes
var arrPageSizes = ___getPageSize();
// Style overlay and show it
$('#jquery-overlay').css({
backgroundColor: settings.overlayBgColor,
opacity: settings.overlayOpacity,
width: arrPageSizes[0],
height: arrPageSizes[1]
}).fadeIn();
// Get page scroll
var arrPageScroll = ___getPageScroll();
// Calculate top and left offset for the jquery-lightbox div object and show it
$('#jquery-lightbox').css({
top: arrPageScroll[1] + (arrPageSizes[3] / 10),
left: arrPageScroll[0]
}).show();
// Assigning click events in elements to close overlay
$('#jquery-overlay,#jquery-lightbox').click(function() {
_finish();
});
// Assign the _finish function to lightbox-loading-link and lightbox-secNav-btnClose objects
$('#lightbox-loading-link,#lightbox-secNav-btnClose').click(function() {
_finish();
return false;
});
// If window was resized, calculate the new overlay dimensions
$(window).resize(function() {
// Get page sizes
var arrPageSizes = ___getPageSize();
// Style overlay and show it
$('#jquery-overlay').css({
width: arrPageSizes[0],
height: arrPageSizes[1]
});
// Get page scroll
var arrPageScroll = ___getPageScroll();
// Calculate top and left offset for the jquery-lightbox div object and show it
$('#jquery-lightbox').css({
top: arrPageScroll[1] + (arrPageSizes[3] / 10),
left: arrPageScroll[0]
});
});
}
/**
* Prepares image exibition; doing a image´s preloader to calculate it´s size
*
*/
function _set_image_to_view() { // show the loading
// Show the loading
$('#lightbox-loading').show();
if ( settings.fixedNavigation ) {
$('#lightbox-image,#lightbox-container-image-data-box,#lightbox-image-details-currentNumber').hide();
} else {
// Hide some elements
$('#lightbox-image,#lightbox-nav,#lightbox-nav-btnPrev,#lightbox-nav-btnNext,#lightbox-container-image-data-box,#lightbox-image-details-currentNumber').hide();
}
// Image preload process
var objImagePreloader = new Image();
objImagePreloader.onload = function() {
$('#lightbox-image').attr('src',settings.imageArray[settings.activeImage][0]);
// Perfomance an effect in the image container resizing it
_resize_container_image_box(objImagePreloader.width,objImagePreloader.height);
// clear onLoad, IE behaves irratically with animated gifs otherwise
objImagePreloader.onload=function(){};
};
objImagePreloader.src = settings.imageArray[settings.activeImage][0];
};
/**
* Perfomance an effect in the image container resizing it
*
* @param integer intImageWidth The image´s width that will be showed
* @param integer intImageHeight The image´s height that will be showed
*/
function _resize_container_image_box(intImageWidth,intImageHeight) {
// Get current width and height
var intCurrentWidth = $('#lightbox-container-image-box').width();
var intCurrentHeight = $('#lightbox-container-image-box').height();
// Get the width and height of the selected image plus the padding
var intWidth = (intImageWidth + (settings.containerBorderSize * 2)); // Plus the image´s width and the left and right padding value
var intHeight = (intImageHeight + (settings.containerBorderSize * 2)); // Plus the image´s height and the left and right padding value
// Diferences
var intDiffW = intCurrentWidth - intWidth;
var intDiffH = intCurrentHeight - intHeight;
// Perfomance the effect
$('#lightbox-container-image-box').animate({ width: intWidth, height: intHeight },settings.containerResizeSpeed,function() { _show_image(); });
if ( ( intDiffW == 0 ) && ( intDiffH == 0 ) ) {
if ( $.browser.msie ) {
___pause(250);
} else {
___pause(100);
}
}
$('#lightbox-container-image-data-box').css({ width: intImageWidth });
$('#lightbox-nav-btnPrev,#lightbox-nav-btnNext').css({ height: intImageHeight + (settings.containerBorderSize * 2) });
};
/**
* Show the prepared image
*
*/
function _show_image() {
$('#lightbox-loading').hide();
$('#lightbox-image').fadeIn(function() {
_show_image_data();
_set_navigation();
});
_preload_neighbor_images();
};
/**
* Show the image information
*
*/
function _show_image_data() {
$('#lightbox-container-image-data-box').slideDown('fast');
$('#lightbox-image-details-caption').hide();
if ( settings.imageArray[settings.activeImage][1] ) {
$('#lightbox-image-details-caption').html(settings.imageArray[settings.activeImage][1]).show();
}
// If we have a image set, display 'Image X of X'
if ( settings.imageArray.length > 1 ) {
$('#lightbox-image-details-currentNumber').html(settings.txtImage + ' ' + ( settings.activeImage + 1 ) + ' ' + settings.txtOf + ' ' + settings.imageArray.length).show();
}
}
/**
* Display the button navigations
*
*/
function _set_navigation() {
$('#lightbox-nav').show();
// Instead to define this configuration in CSS file, we define here. And it´s need to IE. Just.
$('#lightbox-nav-btnPrev,#lightbox-nav-btnNext').css({ 'background' : 'transparent url(' + settings.imageBlank + ') no-repeat' });
// Show the prev button, if not the first image in set
if ( settings.activeImage != 0 ) {
if ( settings.fixedNavigation ) {
$('#lightbox-nav-btnPrev').css({ 'background' : 'url(' + settings.imageBtnPrev + ') left 15% no-repeat' })
.unbind()
.bind('click',function() {
settings.activeImage = settings.activeImage - 1;
_set_image_to_view();
return false;
});
} else {
// Show the images button for Next buttons
$('#lightbox-nav-btnPrev').unbind().hover(function() {
$(this).css({ 'background' : 'url(' + settings.imageBtnPrev + ') left 15% no-repeat' });
},function() {
$(this).css({ 'background' : 'transparent url(' + settings.imageBlank + ') no-repeat' });
}).show().bind('click',function() {
settings.activeImage = settings.activeImage - 1;
_set_image_to_view();
return false;
});
}
}
// Show the next button, if not the last image in set
if ( settings.activeImage != ( settings.imageArray.length -1 ) ) {
if ( settings.fixedNavigation ) {
$('#lightbox-nav-btnNext').css({ 'background' : 'url(' + settings.imageBtnNext + ') right 15% no-repeat' })
.unbind()
.bind('click',function() {
settings.activeImage = settings.activeImage + 1;
_set_image_to_view();
return false;
});
} else {
// Show the images button for Next buttons
$('#lightbox-nav-btnNext').unbind().hover(function() {
$(this).css({ 'background' : 'url(' + settings.imageBtnNext + ') right 15% no-repeat' });
},function() {
$(this).css({ 'background' : 'transparent url(' + settings.imageBlank + ') no-repeat' });
}).show().bind('click',function() {
settings.activeImage = settings.activeImage + 1;
_set_image_to_view();
return false;
});
}
}
// Enable keyboard navigation
_enable_keyboard_navigation();
}
/**
* Enable a support to keyboard navigation
*
*/
function _enable_keyboard_navigation() {
$(document).keydown(function(objEvent) {
_keyboard_action(objEvent);
});
}
/**
* Disable the support to keyboard navigation
*
*/
function _disable_keyboard_navigation() {
$(document).unbind();
}
/**
* Perform the keyboard actions
*
*/
function _keyboard_action(objEvent) {
// To ie
if ( objEvent == null ) {
keycode = event.keyCode;
escapeKey = 27;
// To Mozilla
} else {
keycode = objEvent.keyCode;
escapeKey = objEvent.DOM_VK_ESCAPE;
}
// Get the key in lower case form
key = String.fromCharCode(keycode).toLowerCase();
// Verify the keys to close the ligthBox
if ( ( key == settings.keyToClose ) || ( key == 'x' ) || ( keycode == escapeKey ) ) {
_finish();
}
// Verify the key to show the previous image
if ( ( key == settings.keyToPrev ) || ( keycode == 37 ) ) {
// If we´re not showing the first image, call the previous
if ( settings.activeImage != 0 ) {
settings.activeImage = settings.activeImage - 1;
_set_image_to_view();
_disable_keyboard_navigation();
}
}
// Verify the key to show the next image
if ( ( key == settings.keyToNext ) || ( keycode == 39 ) ) {
// If we´re not showing the last image, call the next
if ( settings.activeImage != ( settings.imageArray.length - 1 ) ) {
settings.activeImage = settings.activeImage + 1;
_set_image_to_view();
_disable_keyboard_navigation();
}
}
}
/**
* Preload prev and next images being showed
*
*/
function _preload_neighbor_images() {
if ( (settings.imageArray.length -1) > settings.activeImage ) {
objNext = new Image();
objNext.src = settings.imageArray[settings.activeImage + 1][0];
}
if ( settings.activeImage > 0 ) {
objPrev = new Image();
objPrev.src = settings.imageArray[settings.activeImage -1][0];
}
}
/**
* Remove jQuery lightBox plugin HTML markup
*
*/
function _finish() {
$('#jquery-lightbox').remove();
$('#jquery-overlay').fadeOut(function() { $('#jquery-overlay').remove(); });
// Show some elements to avoid conflict with overlay in IE. These elements appear above the overlay.
$('embed, object, select').css({ 'visibility' : 'visible' });
}
/**
/ THIRD FUNCTION
* getPageSize() by quirksmode.com
*
* @return Array Return an array with page width, height and window width, height
*/
function ___getPageSize() {
var xScroll, yScroll;
if (window.innerHeight && window.scrollMaxY) {
xScroll = window.innerWidth + window.scrollMaxX;
yScroll = window.innerHeight + window.scrollMaxY;
} else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac
xScroll = document.body.scrollWidth;
yScroll = document.body.scrollHeight;
} else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
xScroll = document.body.offsetWidth;
yScroll = document.body.offsetHeight;
}
var windowWidth, windowHeight;
if (self.innerHeight) { // all except Explorer
if(document.documentElement.clientWidth){
windowWidth = document.documentElement.clientWidth;
} else {
windowWidth = self.innerWidth;
}
windowHeight = self.innerHeight;
} else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
windowWidth = document.documentElement.clientWidth;
windowHeight = document.documentElement.clientHeight;
} else if (document.body) { // other Explorers
windowWidth = document.body.clientWidth;
windowHeight = document.body.clientHeight;
}
// for small pages with total height less then height of the viewport
if(yScroll < windowHeight){
pageHeight = windowHeight;
} else {
pageHeight = yScroll;
}
// for small pages with total width less then width of the viewport
if(xScroll < windowWidth){
pageWidth = xScroll;
} else {
pageWidth = windowWidth;
}
arrayPageSize = new Array(pageWidth,pageHeight,windowWidth,windowHeight);
return arrayPageSize;
};
/**
/ THIRD FUNCTION
* getPageScroll() by quirksmode.com
*
* @return Array Return an array with x,y page scroll values.
*/
function ___getPageScroll() {
var xScroll, yScroll;
if (self.pageYOffset) {
yScroll = self.pageYOffset;
xScroll = self.pageXOffset;
} else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict
yScroll = document.documentElement.scrollTop;
xScroll = document.documentElement.scrollLeft;
} else if (document.body) {// all other Explorers
yScroll = document.body.scrollTop;
xScroll = document.body.scrollLeft;
}
arrayPageScroll = new Array(xScroll,yScroll);
return arrayPageScroll;
};
/**
* Stop the code execution from a escified time in milisecond
*
*/
function ___pause(ms) {
var date = new Date();
curDate = null;
do { var curDate = new Date(); }
while ( curDate - date < ms);
};
// Return the jQuery object for chaining. The unbind method is used to avoid click conflict when the plugin is called more than once
return this.unbind('click').click(_initialize);
};
})(jQuery); // Call and execute the function immediately passing the jQuery object

View file

@ -1,296 +0,0 @@
/*
Uploadify v2.1.4
Release Date: November 8, 2010
Copyright (c) 2010 Ronnie Garcia, Travis Nickels
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.
*/
if(jQuery)(
function(jQuery){
jQuery.extend(jQuery.fn,{
uploadify:function(options) {
jQuery(this).each(function(){
var settings = jQuery.extend({
id : jQuery(this).attr('id'), // The ID of the object being Uploadified
uploader : 'uploadify.swf', // The path to the uploadify swf file
script : 'uploadify.php', // The path to the uploadify backend upload script
expressInstall : null, // The path to the express install swf file
folder : '', // The path to the upload folder
height : 30, // The height of the flash button
width : 120, // The width of the flash button
cancelImg : 'cancel.png', // The path to the cancel image for the default file queue item container
wmode : 'opaque', // The wmode of the flash file
scriptAccess : 'sameDomain', // Set to "always" to allow script access across domains
fileDataName : 'Filedata', // The name of the file collection object in the backend upload script
method : 'POST', // The method for sending variables to the backend upload script
queueSizeLimit : 999, // The maximum size of the file queue
simUploadLimit : 1, // The number of simultaneous uploads allowed
queueID : false, // The optional ID of the queue container
displayData : 'percentage', // Set to "speed" to show the upload speed in the default queue item
removeCompleted : true, // Set to true if you want the queue items to be removed when a file is done uploading
onInit : function() {}, // Function to run when uploadify is initialized
onSelect : function() {}, // Function to run when a file is selected
onSelectOnce : function() {}, // Function to run once when files are added to the queue
onQueueFull : function() {}, // Function to run when the queue reaches capacity
onCheck : function() {}, // Function to run when script checks for duplicate files on the server
onCancel : function() {}, // Function to run when an item is cleared from the queue
onClearQueue : function() {}, // Function to run when the queue is manually cleared
onError : function() {}, // Function to run when an upload item returns an error
onProgress : function() {}, // Function to run each time the upload progress is updated
onComplete : function() {}, // Function to run when an upload is completed
onAllComplete : function() {} // Function to run when all uploads are completed
}, options);
jQuery(this).data('settings',settings);
var pagePath = location.pathname;
pagePath = pagePath.split('/');
pagePath.pop();
pagePath = pagePath.join('/') + '/';
var data = {};
data.uploadifyID = settings.id;
data.pagepath = pagePath;
if (settings.buttonImg) data.buttonImg = escape(settings.buttonImg);
if (settings.buttonText) data.buttonText = escape(settings.buttonText);
if (settings.rollover) data.rollover = true;
data.script = settings.script;
data.folder = escape(settings.folder);
if (settings.scriptData) {
var scriptDataString = '';
for (var name in settings.scriptData) {
scriptDataString += '&' + name + '=' + settings.scriptData[name];
}
data.scriptData = escape(scriptDataString.substr(1));
}
data.width = settings.width;
data.height = settings.height;
data.wmode = settings.wmode;
data.method = settings.method;
data.queueSizeLimit = settings.queueSizeLimit;
data.simUploadLimit = settings.simUploadLimit;
if (settings.hideButton) data.hideButton = true;
if (settings.fileDesc) data.fileDesc = settings.fileDesc;
if (settings.fileExt) data.fileExt = settings.fileExt;
if (settings.multi) data.multi = true;
if (settings.auto) data.auto = true;
if (settings.sizeLimit) data.sizeLimit = settings.sizeLimit;
if (settings.checkScript) data.checkScript = settings.checkScript;
if (settings.fileDataName) data.fileDataName = settings.fileDataName;
if (settings.queueID) data.queueID = settings.queueID;
if (settings.onInit() !== false) {
jQuery(this).css('display','none');
jQuery(this).after('<div id="' + jQuery(this).attr('id') + 'Uploader"></div>');
swfobject.embedSWF(settings.uploader, settings.id + 'Uploader', settings.width, settings.height, '9.0.24', settings.expressInstall, data, {'quality':'high','wmode':settings.wmode,'allowScriptAccess':settings.scriptAccess},{},function(event) {
if (typeof(settings.onSWFReady) == 'function' && event.success) settings.onSWFReady();
});
if (settings.queueID == false) {
jQuery("#" + jQuery(this).attr('id') + "Uploader").after('<div id="' + jQuery(this).attr('id') + 'Queue" class="uploadifyQueue"></div>');
} else {
jQuery("#" + settings.queueID).addClass('uploadifyQueue');
}
}
if (typeof(settings.onOpen) == 'function') {
jQuery(this).bind("uploadifyOpen", settings.onOpen);
}
jQuery(this).bind("uploadifySelect", {'action': settings.onSelect, 'queueID': settings.queueID}, function(event, ID, fileObj) {
if (event.data.action(event, ID, fileObj) !== false) {
var byteSize = Math.round(fileObj.size / 1024 * 100) * .01;
var suffix = 'KB';
if (byteSize > 1000) {
byteSize = Math.round(byteSize *.001 * 100) * .01;
suffix = 'MB';
}
var sizeParts = byteSize.toString().split('.');
if (sizeParts.length > 1) {
byteSize = sizeParts[0] + '.' + sizeParts[1].substr(0,2);
} else {
byteSize = sizeParts[0];
}
if (fileObj.name.length > 20) {
fileName = fileObj.name.substr(0,20) + '...';
} else {
fileName = fileObj.name;
}
queue = '#' + jQuery(this).attr('id') + 'Queue';
if (event.data.queueID) {
queue = '#' + event.data.queueID;
}
jQuery(queue).append('<div id="' + jQuery(this).attr('id') + ID + '" class="uploadifyQueueItem">\
<div class="cancel">\
<a href="javascript:jQuery(\'#' + jQuery(this).attr('id') + '\').uploadifyCancel(\'' + ID + '\')"><img src="' + settings.cancelImg + '" border="0" /></a>\
</div>\
<span class="fileName">' + fileName + ' (' + byteSize + suffix + ')</span><span class="percentage"></span>\
<div class="uploadifyProgress">\
<div id="' + jQuery(this).attr('id') + ID + 'ProgressBar" class="uploadifyProgressBar"><!--Progress Bar--></div>\
</div>\
</div>');
}
});
jQuery(this).bind("uploadifySelectOnce", {'action': settings.onSelectOnce}, function(event, data) {
event.data.action(event, data);
if (settings.auto) {
if (settings.checkScript) {
jQuery(this).uploadifyUpload(null, false);
} else {
jQuery(this).uploadifyUpload(null, true);
}
}
});
jQuery(this).bind("uploadifyQueueFull", {'action': settings.onQueueFull}, function(event, queueSizeLimit) {
if (event.data.action(event, queueSizeLimit) !== false) {
alert('The queue is full. The max size is ' + queueSizeLimit + '.');
}
});
jQuery(this).bind("uploadifyCheckExist", {'action': settings.onCheck}, function(event, checkScript, fileQueueObj, folder, single) {
var postData = new Object();
postData = fileQueueObj;
postData.folder = (folder.substr(0,1) == '/') ? folder : pagePath + folder;
if (single) {
for (var ID in fileQueueObj) {
var singleFileID = ID;
}
}
jQuery.post(checkScript, postData, function(data) {
for(var key in data) {
if (event.data.action(event, data, key) !== false) {
var replaceFile = confirm("Do you want to replace the file " + data[key] + "?");
if (!replaceFile) {
document.getElementById(jQuery(event.target).attr('id') + 'Uploader').cancelFileUpload(key,true,true);
}
}
}
if (single) {
document.getElementById(jQuery(event.target).attr('id') + 'Uploader').startFileUpload(singleFileID, true);
} else {
document.getElementById(jQuery(event.target).attr('id') + 'Uploader').startFileUpload(null, true);
}
}, "json");
});
jQuery(this).bind("uploadifyCancel", {'action': settings.onCancel}, function(event, ID, fileObj, data, remove, clearFast) {
if (event.data.action(event, ID, fileObj, data, clearFast) !== false) {
if (remove) {
var fadeSpeed = (clearFast == true) ? 0 : 250;
jQuery("#" + jQuery(this).attr('id') + ID).fadeOut(fadeSpeed, function() { jQuery(this).remove() });
}
}
});
jQuery(this).bind("uploadifyClearQueue", {'action': settings.onClearQueue}, function(event, clearFast) {
var queueID = (settings.queueID) ? settings.queueID : jQuery(this).attr('id') + 'Queue';
if (clearFast) {
jQuery("#" + queueID).find('.uploadifyQueueItem').remove();
}
if (event.data.action(event, clearFast) !== false) {
jQuery("#" + queueID).find('.uploadifyQueueItem').each(function() {
var index = jQuery('.uploadifyQueueItem').index(this);
jQuery(this).delay(index * 100).fadeOut(250, function() { jQuery(this).remove() });
});
}
});
var errorArray = [];
jQuery(this).bind("uploadifyError", {'action': settings.onError}, function(event, ID, fileObj, errorObj) {
if (event.data.action(event, ID, fileObj, errorObj) !== false) {
var fileArray = new Array(ID, fileObj, errorObj);
errorArray.push(fileArray);
jQuery("#" + jQuery(this).attr('id') + ID).find('.percentage').text(" - " + errorObj.type + " Error");
jQuery("#" + jQuery(this).attr('id') + ID).find('.uploadifyProgress').hide();
jQuery("#" + jQuery(this).attr('id') + ID).addClass('uploadifyError');
}
});
if (typeof(settings.onUpload) == 'function') {
jQuery(this).bind("uploadifyUpload", settings.onUpload);
}
jQuery(this).bind("uploadifyProgress", {'action': settings.onProgress, 'toDisplay': settings.displayData}, function(event, ID, fileObj, data) {
if (event.data.action(event, ID, fileObj, data) !== false) {
jQuery("#" + jQuery(this).attr('id') + ID + "ProgressBar").animate({'width': data.percentage + '%'},250,function() {
if (data.percentage == 100) {
jQuery(this).closest('.uploadifyProgress').fadeOut(250,function() {jQuery(this).remove()});
}
});
if (event.data.toDisplay == 'percentage') displayData = ' - ' + data.percentage + '%';
if (event.data.toDisplay == 'speed') displayData = ' - ' + data.speed + 'KB/s';
if (event.data.toDisplay == null) displayData = ' ';
jQuery("#" + jQuery(this).attr('id') + ID).find('.percentage').text(displayData);
}
});
jQuery(this).bind("uploadifyComplete", {'action': settings.onComplete}, function(event, ID, fileObj, response, data) {
if (event.data.action(event, ID, fileObj, unescape(response), data) !== false) {
jQuery("#" + jQuery(this).attr('id') + ID).find('.percentage').text(' - Completed');
if (settings.removeCompleted) {
jQuery("#" + jQuery(event.target).attr('id') + ID).fadeOut(250,function() {jQuery(this).remove()});
}
jQuery("#" + jQuery(event.target).attr('id') + ID).addClass('completed');
}
});
if (typeof(settings.onAllComplete) == 'function') {
jQuery(this).bind("uploadifyAllComplete", {'action': settings.onAllComplete}, function(event, data) {
if (event.data.action(event, data) !== false) {
errorArray = [];
}
});
}
});
},
uploadifySettings:function(settingName, settingValue, resetObject) {
var returnValue = false;
jQuery(this).each(function() {
if (settingName == 'scriptData' && settingValue != null) {
if (resetObject) {
var scriptData = settingValue;
} else {
var scriptData = jQuery.extend(jQuery(this).data('settings').scriptData, settingValue);
}
var scriptDataString = '';
for (var name in scriptData) {
scriptDataString += '&' + name + '=' + scriptData[name];
}
settingValue = escape(scriptDataString.substr(1));
}
returnValue = document.getElementById(jQuery(this).attr('id') + 'Uploader').updateSettings(settingName, settingValue);
});
if (settingValue == null) {
if (settingName == 'scriptData') {
var returnSplit = unescape(returnValue).split('&');
var returnObj = new Object();
for (var i = 0; i < returnSplit.length; i++) {
var iSplit = returnSplit[i].split('=');
returnObj[iSplit[0]] = iSplit[1];
}
returnValue = returnObj;
}
}
return returnValue;
},
uploadifyUpload:function(ID,checkComplete) {
jQuery(this).each(function() {
if (!checkComplete) checkComplete = false;
document.getElementById(jQuery(this).attr('id') + 'Uploader').startFileUpload(ID, checkComplete);
});
},
uploadifyCancel:function(ID) {
jQuery(this).each(function() {
document.getElementById(jQuery(this).attr('id') + 'Uploader').cancelFileUpload(ID, true, true, false);
});
},
uploadifyClearQueue:function() {
jQuery(this).each(function() {
document.getElementById(jQuery(this).attr('id') + 'Uploader').clearFileUploadQueue(false);
});
}
})
})(jQuery);

View file

@ -1,5 +0,0 @@
$(function() {
// this space intentionally left blank
})

View file

@ -1,7 +1,5 @@
$(function() {
$('input[name="email"]').focus()
$('#forgot-password-link').click(function() {
window.location.href = '/forgot-password/' + $('#email').val()
return false

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

View file

@ -237,49 +237,6 @@ module Stormy
end # Accounts
module Admins
include Stormy::Models
def setup_admins
@admin_data ||= fixtures('admins')['sami']
@admin = Admin.create(@admin_data)
end
def teardown_admins
@admin.delete!
end
end # Admins
module Projects
include Stormy::Models
def projects
@projects ||= fixtures('projects')
end
def setup_projects(options = {})
account = options[:owner] || @existing_account
@existing_project_data = projects['stormy']
if account
@existing_project_data['account_id'] = account.id
end
@existing_project = Project.create(@existing_project_data)
@new_project_data = projects['dating-free']
end
def teardown_projects
Project.list_ids.each do |id|
Project.delete!(id)
end
end
end # Projects
end # Helpers
end

View file

@ -7,7 +7,6 @@ require 'common'
class AccountsControllerTest < Stormy::Test::ControllerCase
include Stormy::Test::Helpers::Accounts
include Stormy::Test::Helpers::Projects
def setup
setup_accounts
@ -32,7 +31,7 @@ class AccountsControllerTest < Stormy::Test::ControllerCase
def test_sign_up_with_valid_data
post '/sign-up', @account_data
assert_redirected '/projects'
assert_redirected '/account'
assert_equal 1, Pony.sent_mail.length
assert mail = Pony.sent_mail.shift
end
@ -80,17 +79,17 @@ class AccountsControllerTest < Stormy::Test::ControllerCase
def test_sign_in_submit
sign_in
assert_redirected '/projects'
assert_redirected '/account'
end
def test_sign_in_remember
sign_in(@existing_account_data, 'remember' => 'on')
assert_redirected '/projects'
assert_redirected '/account'
post '/sign-out'
assert_redirected '/'
get '/projects'
get '/account'
assert_ok
# deletes remembered cookie
@ -154,7 +153,7 @@ class AccountsControllerTest < Stormy::Test::ControllerCase
new_password = 'new password'
post '/account/reset-password', { 'password' => new_password }
assert_redirected '/projects'
assert_redirected '/account'
assert_equal @existing_account.id, Account.check_password(@existing_account.email, new_password)
assert Account.fetch(@existing_account.id).password == new_password
@ -256,7 +255,7 @@ class AccountsControllerTest < Stormy::Test::ControllerCase
def test_verify_email
get "/account/verify/#{@existing_account.email}/#{@existing_account.email_verification_token}"
assert_redirected '/account'
assert_nil Account.fetch(@existing_account.id).email_verification_token
assert redis.hget(@existing_account.id, 'email_verification_token').blank?
end
def test_verify_email_with_invalid_token_signed_in

View file

@ -7,89 +7,33 @@ require 'common'
class AdminControllerTest < Stormy::Test::ControllerCase
include Stormy::Test::Helpers::Accounts
include Stormy::Test::Helpers::Projects
include Stormy::Helpers::Admin
include Stormy::Helpers::FAQ
include Stormy::Helpers::Utils
def admins
@admins ||= fixtures('admins')
end
def setup
@existing_admin_data = admins['sami']
@existing_admin = Admin.create(@existing_admin_data)
@admin_data = admins['freddy']
setup_accounts
end
def teardown
post '/admin/sign-out'
Admin.list_ids.each do |id|
Admin.delete!(id)
end
post '/sign-out'
teardown_accounts
end
def sign_in(admin = @existing_admin_data)
post '/admin/sign-in', admin
def sign_in(admin = @existing_account_data)
post '/sign-in', admin
end
#####################
### Sign In & Out ###
#####################
def test_sign_in
get '/admin/sign-in'
assert_ok
end
def test_sign_in_submit
sign_in
assert_redirected '/admin'
end
def test_sign_in_with_invalid_credentials
sign_in(@admin_data)
assert_redirected '/admin/sign-in'
end
def test_sign_in_redirect
sign_in
assert_redirected '/admin'
end
def test_sign_out
post '/admin/sign-out'
assert_redirected '/admin'
end
############################
### Dashboard & Password ###
############################
#################
### Dashboard ###
#################
def test_dashboard
sign_in
get '/admin'
assert_ok
assert last_response.body.match(/<title>Dashboard/)
end
def test_change_password
sign_in
get '/admin/password'
assert_ok
new_password = 'new password'
post '/admin/password', { 'password' => new_password, 'password_confirmation' => new_password }
assert_redirected '/admin'
@existing_admin.reload!
assert @existing_admin.password == new_password
# incorrect confirmation
post '/admin/password', { 'password' => new_password, 'password_confirmation' => 'oops' }
assert_redirected '/admin/password'
assert last_response.body.match(/<title>[^<]*Dashboard[^<]*<\/title>/)
end
@ -104,7 +48,6 @@ class AdminControllerTest < Stormy::Test::ControllerCase
end
def test_account
setup_accounts
sign_in
get '/admin/account/' + @existing_account.email
@ -113,12 +56,9 @@ class AdminControllerTest < Stormy::Test::ControllerCase
get '/admin/account/not@an.account'
# this was the previous listing, kind of weird but meh
assert_redirected '/admin/account/' + @existing_account.email
teardown_accounts
end
def test_update_account
setup_accounts
sign_in
# redirected to proper page when changing email addresses
@ -162,94 +102,34 @@ class AdminControllerTest < Stormy::Test::ControllerCase
assert_equal 'Samson', @existing_account.first_name
assert_equal 'Simpson', @existing_account.last_name
assert_equal '+12501234567', @existing_account.phone
teardown_accounts
end
def test_sign_in_as_user
setup_accounts
sign_in
get '/admin/sign-in-as/' + @existing_account.email
assert_equal @existing_account.id, session[:id]
assert_redirected '/projects'
teardown_accounts
assert_redirected '/account'
end
def test_delete_account
setup_accounts
setup_projects
sign_in
# make sure the last listing is marked so we are redirected correctly
get '/admin/accounts'
assert_ok
get "/admin/account/#{@existing_account.email}/delete"
@other_account = Account.create(@account_data)
get "/admin/account/#{@other_account.email}/delete"
assert_redirected '/admin/accounts'
assert_nil Account.fetch(@existing_account.id)
assert_nil Project.fetch(@existing_project.id)
assert_nil Account.fetch(@other_account.id)
# non-existent accounts are already gone, so no problem
get "/admin/account/nobody@nowhere.net/delete"
# this time the last listing was not marked, so we are redirected to the dashboard
assert_redirected '/admin'
teardown_projects
teardown_accounts
end
################
### Projects ###
################
def test_projects
setup_accounts
setup_projects
sign_in
get '/admin/projects'
assert_ok
teardown_projects
teardown_accounts
end
def test_project
setup_accounts
setup_projects
sign_in
# non-existent project
get '/admin/project/999'
assert_redirected '/admin'
# existing project
get '/admin/project/' + @existing_project.id
assert_ok
teardown_projects
teardown_accounts
end
def test_delete_project
setup_accounts
setup_projects
sign_in
# make sure the last listing is marked so we are redirected correctly
get '/admin/projects'
get "/admin/project/#{@existing_project.id}/delete"
assert_redirected '/admin/projects'
assert_nil Project.fetch(@existing_project.id)
teardown_projects
teardown_accounts
end
@ -276,51 +156,4 @@ class AdminControllerTest < Stormy::Test::ControllerCase
self.faq = original_faq
end
######################
### Admin Accounts ###
######################
def test_admins
sign_in
get '/admin/admins'
assert_ok
end
def test_add_admin
sign_in
password = 'password'
fields = {
'name' => 'Freddy Kruger',
'email' => 'freddy@example.com',
'password' => password,
'password_confirmation' => password
}
post '/admin/admins', fields
assert_redirected '/admin/admins'
admin = Admin.fetch_by_email('freddy@example.com')
assert admin.password == password
assert_equal fields['name'], admin.name
assert_equal fields['email'], admin.email
# passwords do not match
fields = {
'name' => 'Jason Vorhees',
'email' => 'jason@example.com',
'password' => 'my password',
'password_confirmation' => 'not the same password'
}
post '/admin/admins', fields
assert_redirected '/admin/admins'
assert_nil Admin.fetch_by_email('jason@example.com')
end
def test_delete_admin
sign_in
get "/admin/admins/#{@existing_admin.id}/delete"
assert_redirected '/admin/admins'
assert_equal 0, Admin.count
end
end

View file

@ -1,262 +0,0 @@
#!/usr/bin/env ruby
#
# Copyright 2011 Beta Street Media
require 'common'
class ProjectsControllerTest < Stormy::Test::ControllerCase
include Stormy::Test::Helpers::Accounts
include Stormy::Test::Helpers::Admins
include Stormy::Test::Helpers::Projects
include Stormy::Helpers::Authorization
def setup
header 'User-Agent', "rack/test (#{Rack::Test::VERSION})"
setup_accounts
setup_projects
sign_in
@updated_project_data ||= {
:id => @existing_project.id,
:name => 'the super amazing project'
}
end
def teardown
teardown_projects
teardown_accounts
end
def create_other_account_and_project
@other_account = Account.create(@account_data)
@new_project_data['account_id'] = @other_account.id
@other_project = Project.create(@new_project_data)
end
def photo_filenames
@photo_filenames ||= Dir[photo_file('*.jpg')]
end
def add_photo(filename = photo_filenames.first)
post '/project/add-photo', {
:id => @existing_project.id,
:photo => Rack::Test::UploadedFile.new(filename, 'image/jpeg')
}
@existing_project.reload!
photo_id = @existing_project.photo_ids.last
assert_response_json_ok(
'n' => @existing_project.count_photos,
'photo' => {
'id' => photo_id,
'url' => @existing_project.photo_url(photo_id)
}
)
end
def add_all_photos
photo_filenames.each { |f| add_photo(f) }
end
##################
### Projects ###
##################
def test_projects
# must be authorized
sign_out
get '/projects'
assert_redirected '/sign-in'
# now we can get the projects page
sign_in
get '/projects'
assert_ok
end
def test_project
get "/project/#{@existing_project.id}"
assert_ok
end
def test_project_without_a_name
@existing_project.name = ''
@existing_project.save!
get "/project/#{@existing_project.id}"
assert_ok
end
def test_cannot_access_others_projects
create_other_account_and_project
get "/project/#{@other_project.id}"
assert_redirected '/projects'
follow_redirect!
assert_ok
assert last_response.body.match(/no such project/i)
end
def test_update_project
data = @updated_project_data
post '/project/update', data
assert_redirected "/project/#{data[:id]}"
@existing_project.reload!
data.each do |name, value|
assert_equal value, @existing_project.send(name)
end
end
def test_update_project_with_invalid_fields
expected_name = @existing_project.name
data = {
:id => @existing_project.id,
:name => ''
}
post '/project/update', data
assert_redirected "/project/#{data[:id]}"
@existing_project.reload!
assert_equal expected_name, @existing_project.name
end
def test_update_project_by_admin
setup_admins
post '/admin/sign-in', @admin_data
data = @updated_project_data
post '/project/update', data
assert_redirected "/project/#{data[:id]}"
teardown_admins
end
def test_cannot_update_others_projects
create_other_account_and_project
post '/project/update', { :id => @other_project.id }
assert_redirected '/projects'
follow_redirect!
assert_ok
assert last_response.body.match(/no such project/i)
end
def test_add_photo
# also test /uploadify which is used for photo uploads in IE
%w[/project/add-photo /uploadify].each_with_index do |path, i|
post path, {
:id => @existing_project.id,
# /project/add-photo
:photo => Rack::Test::UploadedFile.new(photo_filenames.first, 'image/jpeg'),
# /uploadify
:Filedata => Rack::Test::UploadedFile.new(photo_filenames.first, 'image/jpeg')
}
@existing_project.reload!
photo_id = @existing_project.photo_ids[i]
assert_response_json_ok({
'n' => i + 1,
'photo' => {
'id' => photo_id,
'url' => @existing_project.photo_url(photo_id)
}
})
end
end
def test_add_photo_fails_at_photo_limit
Project::MaxPhotos.times { add_photo }
post '/project/add-photo', {
:id => @existing_project.id,
:photo => Rack::Test::UploadedFile.new(photo_filenames.first, 'image/jpeg'),
}
assert_response_json_fail('limit')
post '/uploadify', {
:id => @existing_project.id,
:Filedata => Rack::Test::UploadedFile.new(photo_filenames.first, 'image/jpeg'),
}
assert_bad_request
end
def test_add_photo_by_admin
setup_admins
post '/admin/sign-in', @admin_data
post '/project/add-photo', {
:id => @existing_project.id,
:photo => Rack::Test::UploadedFile.new(photo_filenames.first, 'image/jpeg'),
}
@existing_project.reload!
photo_id = @existing_project.photo_ids.last
assert_response_json_ok({
'n' => 1,
'photo' => {
'id' => photo_id,
'url' => @existing_project.photo_url(photo_id)
}
})
teardown_admins
end
def test_remove_photo
add_photo
photo_id = @existing_project.photo_ids.last
post '/project/remove-photo', {
:id => @existing_project.id,
:photo_id => photo_id
}
@existing_project.reload!
assert_response_json_ok('photos' => [])
assert_equal 0, @existing_project.count_photos
end
def test_remove_photo_by_admin
setup_admins
post '/admin/sign-in', @admin_data
add_photo
photo_id = @existing_project.photo_ids.last
post '/project/remove-photo', {
:id => @existing_project.id,
:photo_id => photo_id
}
@existing_project.reload!
assert_response_json_ok('photos' => [])
assert_equal 0, @existing_project.count_photos
teardown_admins
end
def test_reorder_photos
add_all_photos
@existing_project.reload!
photo_ids = @existing_project.photo_ids
# move the first to the end
photo_ids.push(photo_ids.shift)
post '/project/photo-order', {
:id => @existing_project.id,
:order => photo_ids
}
@existing_project.reload!
assert_equal photo_ids, @existing_project.photo_ids
end
def test_reorder_photos_by_admin
setup_admins
post '/admin/sign-in', @admin_data
add_all_photos
@existing_project.reload!
photo_ids = @existing_project.photo_ids
# move the first to the end
photo_ids.push(photo_ids.shift)
post '/project/photo-order', {
:id => @existing_project.id,
:order => photo_ids
}
@existing_project.reload!
assert_equal photo_ids, @existing_project.photo_ids
teardown_admins
end
end

View file

@ -4,6 +4,7 @@
, "email" : "sami@example.com"
, "phone" : "250-216-6216"
, "password" : "super secret"
, "role" : "admin"
}
, "freddy": {
@ -12,5 +13,6 @@
, "email" : "freddy@example.com"
, "phone" : "(250) 555-9999"
, "password" : "even more secreter"
, "role" : "user"
}
}

View file

@ -1,12 +0,0 @@
{ "sami": {
"name" : "Sami"
, "email" : "sami@example.com"
, "password" : "super secret"
}
, "freddy": {
"name" : "Freddy"
, "email" : "freddy@example.com"
, "password" : "even more secreter"
}
}

View file

@ -1,14 +0,0 @@
{ "stormy": {
"name" : "Stormy Weather"
}
, "apps-for-you" : {
"name" : "Apps For You"
, "funded_timestamp" : 1328475981
}
, "dating-free" : {
"name" : "Dating Free"
, "fizzled_timestamp" : 1328475981
}
}

View file

@ -8,7 +8,6 @@ class AccountsHelperTest < Stormy::Test::HelperCase
include Stormy::Helpers::Accounts
include Stormy::Test::Helpers::Accounts
include Stormy::Test::Helpers::Projects
def setup
setup_accounts

View file

@ -16,14 +16,6 @@ class AdminHelperTest < Stormy::Test::HelperCase
assert_equal 0, num_accounts
end
def test_num_admins
assert_equal 0, num_admins
end
def test_num_projects
assert_equal 0, num_projects
end
def test_last_listing
assert_equal '/admin', last_listing
mark_last_listing '/admin/accounts'

View file

@ -8,21 +8,16 @@ class AuthorizationHelperTest < Stormy::Test::HelperCase
include Stormy::Helpers::Authorization
include Stormy::Test::Helpers::Accounts
include Stormy::Test::Helpers::Admins
include Stormy::Test::Helpers::Projects
def setup
setup_accounts
setup_projects
end
def teardown
deauthorize
teardown_request
teardown_accounts
teardown_projects
@current_account = nil
@current_project = nil
end
def teardown_request
@ -69,7 +64,7 @@ class AuthorizationHelperTest < Stormy::Test::HelperCase
end
def url
'/projects'
'/account'
end
end
end
@ -153,116 +148,31 @@ class AuthorizationHelperTest < Stormy::Test::HelperCase
assert_equal @existing_account.id, current_account.id
end
def test_current_project
assert_nil current_project
current_project(@existing_project.id)
assert_equal @existing_project.id, current_project.id
end
def test_project_authorized?
assert !project_authorized?
current_project(@existing_project.id)
assert !project_authorized?
authorize_account(@existing_account.id)
assert project_authorized?
@current_account = nil
other_account = Account.create(@account_data)
authorize_account(other_account)
assert !project_authorized?
other_account.delete!
end
def test_authorize_project_api!
assert_equal not_authorized, catch(:halt) { authorize_project_api!(@existing_project.id) }
assert_content_type 'text/plain'
@content_type = nil
authorize_account(@existing_account.id)
authorize_project_api!(@existing_project.id)
assert_equal fail('no such project'), catch(:halt) { authorize_project_api!('non-existent id') }
@current_account = nil
other_account = Account.create(@account_data)
authorize_account(other_account.id)
assert_equal not_authorized, catch(:halt) { authorize_project_api!(@existing_project.id) }
assert_content_type 'text/plain'
other_account.delete!
end
def test_authorize_project!
assert_redirected('/sign-in') { authorize_project!(@existing_project.id) }
assert !project_authorized?
authorize_account(@existing_account.id)
assert_redirected('/projects') { authorize_project!('non-existent id') }
assert_equal 'No such project.', flash[:warning]
@redirect = nil
authorize_project!(@existing_project.id)
assert_not_redirected
@current_account = nil
other_account = Account.create(@account_data)
authorize_account(other_account.id)
assert_redirected('/projects') { authorize_project!(@existing_project.id) }
assert_equal 'No such project.', flash[:warning]
other_account.delete!
end
def test_authorize_admin
setup_admins
authorize_admin(@admin.id)
assert_equal @admin.id, session[:admin_id]
assert admin_authorized?
teardown_admins
end
def test_deauthorize_admin
setup_admins
authorize_admin(@admin.id)
deauthorize_admin
assert !admin_authorized?
assert !session.has_key?(:admin_id)
teardown_admins
end
def test_admin_authorized?
setup_admins
assert !admin_authorized?
authorize_admin(@admin.id)
authorize_account(@existing_account.id)
assert admin_authorized?
@current_admin = nil
authorize_admin('does not exist')
@current_account = nil
authorize_account('does not exist')
assert !admin_authorized?
teardown_admins
end
def test_admin_authorize!
setup_admins
authorize_admin(@admin.id)
authorize_account(@existing_account.id)
admin_authorize!
assert_not_redirected
assert !session.has_key?(:original_url)
teardown_admins
end
def test_admin_authorize_redirects_to_sign_in
assert_redirected('/admin') { admin_authorize! }
assert_redirected('/sign-in') { admin_authorize! }
assert_equal request.url, session[:original_url]
end
def test_admin_authorize_api!
setup_admins
authorize_admin(@admin.id)
authorize_account(@existing_account.id)
assert_nil catch(:halt) { admin_authorize_api! }
teardown_admins
end
def test_admin_authorize_api_throws_if_unauthorized
@ -270,13 +180,4 @@ class AuthorizationHelperTest < Stormy::Test::HelperCase
assert_content_type 'text/plain'
end
def test_current_admin
setup_admins
assert_nil current_admin
authorize_admin(@admin.id)
assert_equal @admin.id, current_admin.id
teardown_admins
end
end

View file

@ -203,4 +203,11 @@ class ViewsHelperTest < Stormy::Test::HelperCase
assert_equal "<p>42</p>\n", markdown(42)
end
def test_admin_page?
assert admin_page?('/admin')
assert admin_page?('/admin/accounts')
assert !admin_page?('/')
assert !admin_page?('/account')
end
end

View file

@ -7,11 +7,9 @@ require 'common'
class AccountTest < Stormy::Test::Case
include Stormy::Test::Helpers::Accounts
include Stormy::Test::Helpers::Projects
def setup
setup_accounts
setup_projects
@invalid_addresses = [
'invalid email address',
@ -22,7 +20,6 @@ class AccountTest < Stormy::Test::Case
end
def teardown
teardown_projects
teardown_accounts
end
@ -75,7 +72,7 @@ class AccountTest < Stormy::Test::Case
assert Account.verify_email(@existing_account.email, @existing_account.email_verification_token)
account = Account.fetch(@existing_account.id)
assert account.email_verified
assert !account.email_verification_token
assert !account.instance_variable_get('@email_verification_token')
assert !Account.verify_email('non@existent.email', 'insignificant token')
end
@ -90,15 +87,12 @@ class AccountTest < Stormy::Test::Case
# no new token is generated if one is present
token = @existing_account.email_verification_token
@existing_account.create_email_verification_token
assert_equal @existing_account.email_verification_token, token
# a token is generated if necessary
Account.verify_email(@existing_account.email, @existing_account.email_verification_token) # clears token
account = Account.fetch(@existing_account.id)
assert !account.email_verification_token
account.create_email_verification_token
assert account.email_verification_token
assert !account.instance_variable_get('@email_verification_token')
assert account.email_verification_token != token
end
@ -132,7 +126,7 @@ class AccountTest < Stormy::Test::Case
end
def test_create_with_existing_email
assert_raises Account::EmailTakenError do
assert_raises Account::DuplicateFieldError do
Account.new(@existing_account_data).create
end
end
@ -195,46 +189,14 @@ class AccountTest < Stormy::Test::Case
assert Account.fetch_by_email(@existing_account.email).nil?, 'Account was fetched by email after deletion'
# indexes
assert !@existing_account.email_taken?, 'Account email is taken after deletion'
assert !@existing_account.email_taken?(@existing_account.email), 'Account email is taken after deletion'
assert !Account.exists?(@existing_account.id), 'Account exists after deletion'
# projects are deleted
assert_equal [], @existing_account.project_ids
end
def test_name
assert_equal "#{@existing_account.first_name} #{@existing_account.last_name}", @existing_account.name
end
def test_count_projects
assert_equal 1, @existing_account.count_projects
end
def test_project_ids
assert_equal [@existing_project.id], @existing_account.project_ids
end
def test_projects
assert_equal [@existing_project.id], @existing_account.projects.map { |p| p.id }
end
def test_sorted_projects
# make sure created timestamp is in the future ... this stinks
sleep 1
project = Project.create(@new_project_data.merge(:account_id => @existing_account.id))
assert_equal [@existing_project.id, project.id], @existing_account.sorted_projects.map { |p| p.id }
end
def test_add_project_id
@existing_account.add_project_id('fake-project-id')
assert_equal 2, @existing_account.count_projects
end
def test_remove_project_id
@existing_account.remove_project_id(@existing_project.id)
assert_equal 0, @existing_account.count_projects
end
def test_update
original_data = {
'id' => @existing_account.id,
@ -377,7 +339,7 @@ class AccountTest < Stormy::Test::Case
# all should be updated
check_account_fields(@existing_account, updated_data)
# restore fields required for project clean up
# restore fields required for clean up
@existing_account.update!(original_data)
end

View file

@ -1,237 +0,0 @@
#!/usr/bin/env ruby
#
# Copyright 2012 Sami Samhuri <sami@samhuri.net>
require 'common'
class AdminTest < Stormy::Test::Case
def setup
admins = fixtures('admins')
@admin_data = admins['sami']
@admin = Admin.create(@admin_data)
@invalid_addresses = [
'invalid email address',
'invalid@email@address',
'invalid.email.address',
'invalid.email@address'
]
end
def teardown
@admin.delete!
end
### Class Methods
def test_key_from_email
assert_equal @admin.send(:key), Admin.key_from_email(@admin.email)
assert_nil Admin.key_from_email('not a real email')
end
def test_check_password
assert_equal @admin.id, Admin.check_password(@admin.email, @admin_data['password'])
assert_equal @admin.id, Admin.check_password(@admin.email.upcase, @admin_data['password'])
assert !Admin.check_password(@admin.email, 'incorrect password')
assert !Admin.check_password('non@existent.email', 'any password')
end
def test_email_taken?
assert Admin.email_taken?(@admin.email)
assert !Admin.email_taken?('freddy@example.com'), "New email is reported as taken"
end
def test_fetch_existing_by_id
admin = Admin.fetch(@admin.id)
assert admin
assert_equal @admin.id, admin.id
check_admin_fields(admin, @admin_data)
end
def test_fetch_nonexistent_by_id
assert_nil Admin.fetch('this is not a real id')
end
def test_fetch_existing_by_email
admin = Admin.fetch_by_email(@admin.email)
assert admin
assert_equal @admin.id, admin.id
check_admin_fields(admin, @admin_data)
end
def test_fetch_nonexistent_by_email
assert_nil Admin.fetch_by_email('this is not a real email')
end
def test_id_from_email
assert_equal @admin.id, Admin.id_from_email(@admin.email)
assert_nil Admin.id_from_email('not a real email')
end
### Instance Methods
def check_admin_fields(admin, fields)
fields.each do |key, expected|
if key == 'password'
assert admin.password == fields['password'], "<#{fields['password'].inspect}> expected but was <#{admin.password.inspect}>"
else
actual = admin.send(key)
assert_equal expected, actual, "#{key}: <#{expected.inspect}> expected but was <#{actual.inspect}>"
end
end
end
def test_create
assert @admin
assert @admin.id
check_admin_fields(@admin, @admin_data)
# indexes
assert Admin.fetch_by_email(@admin.email)
end
def test_create_with_existing_email
assert_raises Admin::EmailTakenError do
Admin.new(@admin_data).create
end
end
def test_create_with_missing_fields
# name
assert_raises Admin::InvalidDataError do
Admin.create({ 'email' => 'freddy@example.com', 'password' => 'secret password' })
end
assert_raises Admin::InvalidDataError do
Admin.create({ 'name' => ' ', 'email' => 'freddy@example.com', 'password' => 'secret password' })
end
# email
assert_raises Admin::InvalidDataError do
Admin.create({ 'name' => 'Freddy', 'password' => 'secret password' })
end
assert_raises Admin::InvalidDataError do
Admin.create({ 'name' => 'Freddy', 'email' => ' ', 'password' => 'secret password' })
end
# password
assert_raises Admin::InvalidDataError do
Admin.create({ 'name' => 'Freddy', 'email' => 'freddy@example.com' })
end
assert_raises Admin::InvalidDataError do
Admin.create({ 'name' => 'Freddy', 'email' => 'freddy@example.com', 'password' => ' ' })
end
end
def test_create_with_invalid_fields
data = {
'name' => 'Freddy',
'password' => 'secret password'
}
@invalid_addresses.each do |email|
data['email'] = email
assert_raises Admin::InvalidDataError do
Admin.create(data)
end
end
end
def test_delete!
@admin.delete!
assert Admin.fetch(@admin.id).nil?, 'Admin was fetched by id after deletion'
assert Admin.fetch_by_email(@admin.email).nil?, 'Admin was fetched by email after deletion'
# indexes
assert !@admin.email_taken?, 'Admin email is taken after deletion'
assert !Admin.exists?(@admin.id), 'Admin exists after deletion'
end
def test_update
original_data = {
'id' => @admin.id,
'email' => @admin.email,
}
updated_data = {
# updatable
'name' => 'Samson',
# not updatable
'id' => 'should be ignored',
'email' => 'should be ignored',
'password' => 'should be ignored',
}
@admin.update(updated_data)
# should be updated
assert_equal updated_data['name'], @admin.name
# should not be updated
assert_equal original_data['id'], @admin.id
assert_equal original_data['email'], @admin.email
assert @admin.password != updated_data['password']
assert @admin.password == @admin_data['password']
end
def test_update_with_invalid_fields
assert_raises Admin::InvalidDataError do
@admin.update({ 'name' => ' ' })
end
end
def test_update_email
# pretend this address is verified
new_email = 'sami-different@example.com'
old_email = @admin.email
# updates database immediately
@admin.update_email(new_email)
assert_equal new_email, @admin.email
assert_equal new_email, Admin.fetch(@admin.id).email
# index is updated
assert Admin.email_taken?(new_email)
assert !Admin.email_taken?(old_email)
# no change in address is a noop
@admin.update_email(new_email)
# invalid addresses are rejected
@invalid_addresses.each do |email|
assert_raises Admin::InvalidDataError do
@admin.update_email(email)
end
end
end
def test_update_email_changing_only_case
# change only the case
loud_email = @admin.email.upcase
@admin.update_email(loud_email)
assert_equal loud_email, @admin.email
# is still indexed properly
assert Admin.email_taken?(loud_email)
end
def test_update_password
old_password = @admin_data['password']
new_password = 'the new password'
@admin.update_password(old_password, new_password)
assert @admin.password == new_password
assert Admin.fetch(@admin.id).password == new_password
assert_raises Admin::IncorrectPasswordError do
@admin.update_password('incorrect', 'irrelevant')
end
assert_raises Admin::InvalidDataError do
@admin.update_password(new_password, ' ')
end
end
end

View file

@ -9,8 +9,10 @@ class ModelBaseTest < Stormy::Test::Case
def setup
@my_model_class = Class.new(Stormy::Models::Base)
@my_model_class.class_eval do
model_name 'my_model'
field :id, :required => true
field :name, :updatable => true, :required => true
field :name, :updatable => true, :required => true, :indexed => true
field :email, :required => true, :unique => true
field :age, {
:type => :integer,
:required => true,
@ -18,14 +20,8 @@ class ModelBaseTest < Stormy::Test::Case
:validator => proc { |n| n >= 18 }
}
field :verified, :type => :boolean
def create
self.id = UUID.generate unless id.present?
super
end
end
@fields = { 'name' => 'Sami', 'age' => '29' }
@fields = { 'name' => 'Sami', 'age' => '29', 'email' => 'sami@samhuri.net' }
@my_model = @my_model_class.create(@fields)
end
@ -39,15 +35,16 @@ class ModelBaseTest < Stormy::Test::Case
### Class Methods
def test_name
@my_model_class.name 'my_model'
assert_equal 'my_model', @my_model_class.name
@my_model_class.model_name 'my_model'
assert_equal 'my_model', @my_model_class.model_name
end
def test_id_field
# has id by default
# has id
id_field = {
:type => :string,
:required => true
:required => true,
:accessors => true
}
assert_equal(id_field, @my_model_class.fields[:id])
methods = %w[id id=]
@ -60,7 +57,9 @@ class ModelBaseTest < Stormy::Test::Case
name_field = {
:type => :string,
:required => true,
:updatable => true
:updatable => true,
:accessors => true,
:indexed => true
}
assert_equal(name_field, @my_model_class.fields[:name])
methods = %w[name name=]
@ -85,7 +84,7 @@ class ModelBaseTest < Stormy::Test::Case
end
def test_verified_field
verified_field = { :type => :boolean }
verified_field = { :type => :boolean, :accessors => true }
assert_equal(verified_field, @my_model_class.fields[:verified])
methods = %w[verified verified= verified?]
methods.each do |name|
@ -125,7 +124,7 @@ class ModelBaseTest < Stormy::Test::Case
def test_key
id = @my_model.id
key = Stormy.key(@my_model_class.name, id)
key = Stormy.key(@my_model_class.model_name, id)
assert_equal key, @my_model_class.key(id)
end

View file

@ -1,160 +0,0 @@
#!/usr/bin/env ruby
#
# Copyright 2011 Beta Street Media
require 'common'
JPEGHeader = "\xFF\xD8\xFF\xE0\u0000\u0010JFIF"
class ProjectTest < Stormy::Test::Case
include Stormy::Test::Helpers::Accounts
include Stormy::Test::Helpers::Projects
def setup
setup_accounts
setup_projects
@test_photo_path = photo_file('wild-wacky-action-bike.jpg')
end
def teardown
teardown_projects
teardown_accounts
end
def check_project_fields(project, fields)
fields.each do |key, expected|
actual = project.send(key)
assert_equal expected, actual, "#{key}: <#{expected.inspect}> expected but was <#{actual.inspect}>"
end
end
#####################
### Class Methods ###
#####################
def test_fetch_by_name
project = Project.fetch_by_name(@existing_project.name)
assert project
assert_equal @existing_project.id, project.id
check_project_fields(project, @existing_project_data)
end
def test_fetch_nonexistent_by_name
assert_nil Project.fetch_by_name('non-existent')
end
########################
### Instance Methods ###
########################
def test_create
assert @existing_project
assert @existing_project.id
check_project_fields(@existing_project, @existing_project_data)
# ensure created time is set
# (timestamps may have been a second or two ago at this point, give some allowance for that)
created_delta = Time.now.to_i - @existing_project.created_timestamp
assert created_delta < 3
# adds iteslf to project id list on associated account
assert @existing_project.account.project_ids.include?(@existing_project.id)
end
def test_save_with_missing_fields
Project.fields.each do |name, options|
if options[:required]
orig_value = @existing_project.send(name)
@existing_project.send("#{name}=", nil)
assert_raises Project::InvalidDataError, "#{name} should be required" do
@existing_project.save
end
empty_value =
case options[:type]
when :string
' '
when :integer
0
else
' '
end
@existing_project.send("#{name}=", empty_value)
assert_raises Project::InvalidDataError, "#{name} should be required" do
@existing_project.save
end
@existing_project.send("#{name}=", orig_value)
end
end
end
def test_delete!
@existing_project.delete!
assert Project.fetch(@existing_project.id).nil?, 'Project was fetched by id after deletion'
assert !Project.exists?(@existing_project.id), 'Project exists after deletion'
# removes iteslf from project id list on associated account
assert !@existing_project.account.project_ids.include?(@existing_project.id)
end
def test_count_photos
10.times do |i|
assert_equal i, @existing_project.count_photos
@existing_project.add_photo(@test_photo_path)
assert_equal i + 1, @existing_project.count_photos
end
end
def test_add_photo
data = @existing_project.add_photo(@test_photo_path)
assert_equal 1, @existing_project.count_photos
path = @existing_project.send(:photo_path, data['id'])
assert File.exists?(path)
assert_equal JPEGHeader, File.read(path, JPEGHeader.length)
end
def test_remove_photo
data = @existing_project.add_photo(@test_photo_path)
path = @existing_project.send(:photo_path, data['id'])
@existing_project.remove_photo(data['id'])
assert !File.exists?(path)
assert_equal 0, @existing_project.count_photos
end
def test_photo_data
data = @existing_project.add_photo(@test_photo_path)
assert_equal data, @existing_project.photo_data(data['id'])
end
def test_photo_urls
data = @existing_project.add_photo(@test_photo_path)
urls = @existing_project.photo_urls
assert_equal 1, urls.length
url = urls.first
assert_equal "/photos/#{@existing_project.id}/#{data['id']}.jpg", url
assert_equal data['url'], url
end
def test_photo_paths
@existing_project.add_photo(@test_photo_path)
assert_equal 1, @existing_project.photo_paths.length
end
def test_photos
assert_equal [], @existing_project.photos
data = nil
5.times do |i|
data = @existing_project.add_photo(@test_photo_path)
end
assert_equal [data] * 5, @existing_project.photos
end
def test_account
assert @existing_project.account
assert_equal @existing_account.id, @existing_project.account.id
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View file

@ -38,7 +38,7 @@ class ConfigTest < Stormy::Test::Case
def test_method_missing_get
# (none yet)
# assert_equal defaults['demo_project_id'], @config.demo_project_id
# assert_equal defaults['foo_bar'], @config.foo_bar
end
def test_method_missing_get_default

View file

@ -1,65 +1,82 @@
<h2 class="section-heading top">Account</h2>
<%== flash_message %>
<table id="account">
<tr>
<th>First name</th>
<td><span id="first_name" class="editable"><%= @account.first_name %></span></td>
<td rowspan="4" class="edit-instructions">
&larr; click to edit
</td>
</tr>
<tr>
<th>Last name</th>
<td><span id="last_name" class="editable"><%= @account.last_name %></span></td>
</tr>
<tr>
<th>Email</th>
<td>
<span id="email" class="editable-json"><%= @account.email %></span>
<span id="email-verified" class="verified <%= @account.email_verified? ? '' : 'hidden' %>">&#10003; Verified</span>
</td>
</tr>
<tr>
<th>Phone</th>
<td>
<span id="phone" class="editable"><%= @account.phone %></span>
</td>
</tr>
</table>
<div class="container">
<div class="content">
<p class="indented"><a href="#" id="change-password-link">Change my password</a></p>
<div class="row">
<div class="fields span6">
<h2>Your Account</h2>
<form action="/account/password" method="post" id="change-password-form">
<table id="change-password">
<tr>
<th><label for="old-password">Current password</label></th>
<td><input type="password" id="old-password" name="old-password" placeholder="Current password"></td>
</tr>
<tr>
<th><label for="new-password">New password</label></th>
<td><input type="password" id="new-password" name="new-password" placeholder="Secret"></td>
</tr>
<tr>
<th><label for="password-confirmation">Confirm</label></th>
<td><input type="password" id="password-confirmation" name="password-confirmation" placeholder="Again"></td>
</tr>
<tr>
<th></th>
<td align="right">
<input type="submit" id="change-password-button" value="Change My Password">
<img class="spinner" src="/images/spinner.gif">
</td>
</tr>
</table>
</form>
<div class="clearfix">
<p>
<strong>First name</strong>
<br>
<span id="first_name" class="editable"><%= @account.first_name %></span>
</p>
</div>
<p id="password-changed" class="indented">Password changed!</p>
<div class="clearfix">
<p>
<strong>Last name</strong>
<br>
<span id="last_name" class="editable"><%= @account.last_name %></span>
</p>
</div>
<div id="email-verification" class="indented <%= @account.email_verified? ? 'hidden' : '' %>">
<hr>
<p id="unverified-email" class="unverified">Your email address is unverified.</p>
<p><a href="#" id="send-email-verification">Send verification email</a></p>
<p id="sending-email-verification"><img src="/images/spinner.gif"> Sending...</p>
<div class="clearfix">
<p>
<strong>Phone</strong>
<br>
<span id="phone" class="editable"><%= @account.phone %></span>
</p>
</div>
<div class="clearfix">
<span id="email-verified" class="verified <%= @account.email_verified? ? '' : 'hidden' %>">&#10003; Verified</span>
<p>
<strong>Email</strong>
<br>
<span id="email" class="editable-json"><%= @account.email %></span>
</p>
</div>
<div id="email-verification" class="clearfix <%= @account.email_verified? ? 'hidden' : '' %>">
<p id="unverified-email" class="unverified">Your email address is unverified.</p>
<p><button id="send-email-verification" class="btn">Send verification email</button></p>
<p id="sending-email-verification" class="hidden"><img src="/images/spinner.gif"> Sending...</p>
</div>
</div> <!-- fields -->
<div class="fields span6">
<hr class="separator">
<h2>Password</h2>
<form action="/account/password" method="post" id="change-password-form">
<fieldset>
<div class="clearfix">
<label for="old-password">Current password</label>
<input type="password" id="old-password" name="old-password" placeholder="Current password">
</div>
<div class="clearfix">
<label for="new-password">New password</label>
<input type="password" id="new-password" name="new-password" placeholder="Secret">
</div>
<div class="clearfix">
<label for="password-confirmation">Confirm</label>
<input type="password" id="password-confirmation" name="password-confirmation" placeholder="Again">
</div>
<div class="clearfix">
<input type="submit" id="change-password-button" class="btn" value="Change my password">
<span id="change-password-spinner" class="hidden"><img class="spinner" src="/images/spinner.gif"> Changing...</span>
</div>
<p id="password-changed" class="notice hidden">Password changed!</p>
</fieldset>
</form>
</div> <!-- fields -->
</div> <!-- row -->
</div>
</div>

View file

@ -4,7 +4,6 @@
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Projects</th>
</tr>
<% for account in @accounts %>
@ -12,7 +11,6 @@
<td class="name"><a href="/admin/account/<%= account.email %>"><%= account.name %></a></td>
<td><%= account.email %></td>
<td><%= account.phone %></td>
<td><%= account.count_projects %></td>
</tr>
<% end %>
</table>

View file

@ -1,44 +0,0 @@
<p>Listing <%= @admins.length %> admin accounts</p>
<table id="accounts" width="90%">
<tr>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
<% for admin in @admins %>
<tr>
<td class="name"><%= admin.name %></td>
<td><%= admin.email %></td>
<td><a href="/admin/admins/<%= admin.id %>/delete">Delete</a></td>
</tr>
<% end %>
</table>
<h2>Add an admin</h2>
<form method="post">
<table>
<tr>
<th>Name</th>
<td><input type="text" name="name" size="30" value="<%= @fields['name'] %>"></td>
</tr>
<tr>
<th>Email</th>
<td><input type="email" name="email" size="30" value="<%= @fields['email'] %>"></td>
</tr>
<tr>
<th>Password</th>
<td><input type="password" name="password" size="30"></td>
</tr>
<tr>
<th>Confirm</th>
<td><input type="password" name="password_confirmation" size="30"></td>
</tr>
<tr>
<td colspan="2" align="right">
<input type="submit" value="Add admin">
</td>
</tr>
</table>
</form>

View file

@ -1,48 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title><%= @page_title %> - Stormy Weather Admin</title>
<link rel="stylesheet" href="/css/admin.css" charset="utf-8">
<% if @page_styles %>
<% for url in @page_styles %>
<link rel="stylesheet" href="<%= url %>" charset="utf-8">
<% end %>
<% end %>
</head>
<body>
<h1><%= @page_title %></h1>
<% if current_admin %>
<div id="sign-out">
Signed in as <%= current_admin.name %>.
<a href="#" id="sign-out-link">Sign Out</a>
<br>
<a href="/admin/password">Change password</a>
<form id="sign-out-form" action="/admin/sign-out" method="post"></form>
</div>
<%== flash_message %>
<ul id="nav">
<li><a href="/admin">Dashboard</a></li>
<li><a href="/admin/accounts">Accounts (<%= num_accounts %>)</a></li>
<li><a href="/admin/projects">Project IDs (<%= num_projects %>)</a></li>
<li><a href="/admin/faq">FAQ</a></li>
<li><a href="/admin/admins">Admin Accounts (<%= num_admins %>)</a></li>
</ul>
<% end %>
<div id="content">
<%== yield %>
</div>
<% for url in scripts %>
<script src="<%= url %>"></script>
<% end %>
</body>
</html>

View file

@ -1,45 +0,0 @@
<p><a href="/admin/projects">&lt;&lt; Projects</a></p>
<table id="project">
<tr>
<th>ID</th>
<td><%= @project.id %></td>
</tr>
<tr>
<th>Owner</th>
<td><a href="/admin/account/<%= @project.account.email %>"><%= @project.account.name %></a></td>
</tr>
<tr>
<th>Name</th>
<td><%= @project.name %></td>
</tr>
<tr>
<th>Created</th>
<td><%= format_date(Time.at(@project.created_timestamp)) %></td>
</tr>
<% if @project.funded? %>
<tr>
<th>Funded</th>
<td><%= format_date(Time.at(@project.funded_timestamp)) %></td>
</tr>
<% end %>
<% if @project.fizzled? %>
<tr>
<th>Fizzled</th>
<td><%= format_date(Time.at(@project.fizzled_timestamp)) %></td>
</tr>
<% end %>
</table>
<h2>Actions</h2>
<p align="center">
<a id="delete" href="/admin/project/<%= @project.id %>/delete">Delete this project</a>
</p>
<h2>Photos</h2>
<div>
<% for url in @project.photo_urls %>
<a href="<%= url %>"><img src="<%= url %>" alt=""></a>
<br>
<% end %>
</div>

View file

@ -1,18 +0,0 @@
<table id="projects">
<tr>
<th>Name</th>
<th>Owner</th>
<th>Created</th>
<th>Fizzled</th>
<th>Funded</th>
</tr>
<% for project in @projects %>
<tr>
<td><a href="/admin/project/<%= project.id %>"><%= project.name %></a></td>
<td><a href="/admin/account/<%= project.account.email %>"><%= project.account.name %></a></td>
<td><%= format_date(Time.at(project.created_timestamp)) %>
<td><%= project.fizzled? ? format_date(Time.at(project.fizzled_timestamp)) : '-' %>
<td><%= project.funded? ? format_date(Time.at(project.funded_timestamp)) : '-' %>
</tr>
<% end %>
</table>

View file

@ -1,27 +0,0 @@
<%== flash_message %>
<form id="sign-in-form" action="/admin/sign-in" method="post">
<table id="sign-in">
<tr>
<th><label for="email">Email</label></th>
<td><input type="email" name="email" id="email" size="30" placeholder="jane.doe@example.com" value="<%= @email %>"></td>
</tr>
<tr>
<th><label for="password">Password</label></th>
<td id="password-cell">
<input type="password" name="password" id="password" size="30" placeholder="Secret">
</td>
</tr>
<tr>
<th></th>
<td id="submit-cell">
<input id="sign-in-button" type="submit" value="Sign In">
<span id="sign-in-spinner"><img src="/images/spinner.gif"> Signing in...</span>
</td>
</tr>
</table>
</form>

View file

@ -1,12 +1,12 @@
<div id="about-us">
<h2 class="section-heading top">About Us</h2>
<h3 class="section-heading top">About Us</h3>
<p class="section">
Made of 100% pure awesome. #winning
</p>
</div>
<div id="contact-us">
<h2 class="section-heading top">Contact Us</h2>
<h3 class="section-heading top">Contact Us</h3>
<%== flash_message %>
@ -23,28 +23,28 @@
</div>
<div id="contact-email">
<h2 class="section-heading">Email</h2>
<h3 class="section-heading">Email</h3>
<p class="section">Contact us via email.</p>
<table class="section">
<tr>
<th>Technical support:</th>
<td><a href="mailto:tech@example.com">tech@example.com</a></td>
<td><a href="mailto:tech@example.comapult.com">tech@example.comapult.com</a></td>
</tr>
<tr>
<th>Accounting support:</th>
<td><a href="mailto:accounts@example.com">accounts@example.com</a></td>
<td><a href="mailto:accounts@example.comapult.com">accounts@example.comapult.com</a></td>
</tr>
<tr>
<th>General inquiries:</th>
<td><a href="mailto:info@example.com">info@example.com</a></td>
<td><a href="mailto:info@example.comapult.com">info@example.comapult.com</a></td>
</tr>
</table>
</div>
<div id="address-phone">
<h2 class="section-heading">Address and Phone</h2>
<h3 class="section-heading">Address and Phone</h3>
<p class="section">Or keep it old school.</p>

View file

@ -1,80 +0,0 @@
<h2 class="section-heading top">Project Info</h2>
<%== flash_message %>
<![if !IE]>
<iframe id="upload-target" name="upload-target"></iframe>
<form id="photo-form" action="/project/add-photo" method="post" enctype="multipart/form-data" target="upload-target">
<input type="hidden" name="id" value="<%= @project.id %>">
<input type="file" id="photo-uploader" name="photo">
</form>
<![endif]>
<form action="/project/update" method="post" id="project">
<input type="hidden" name="id" value="<%= @project.id %>">
<p class="save">
<input type="submit" class="button save-button" value="Save <%= @project.name.present? ? @project.name : 'This Project' %>">
<span class="save-button-spinner"><img src="/images/spinner.gif"> Saving...</span>
</p>
<table id="project-info" class="section">
<tr>
<th><label for="name">Name</label></th>
<td>
<input type="text" id="name" name="name" size="40" placeholder="Wild Wacky Action Bike" value="<%= @project.name %>">
</td>
</tr>
</table>
<h2 class="section-heading">Photos</h2>
<script type="text/html" id="photo-template">
<li class="photo" id="photo-<%%= id %>">
<a href="<%%= url %>" class="thumbnail" id="thumbnail-<%%= id %>"><img src="<%%= url %>" width="64" height="64"></a>
<br>
<a href="#" class="remove-photo" id="remove-photo-<%%= id %>">remove</a>
</li>
</script>
<div class="section" id="photos-container">
<ul id="photos">
<% for photo in @project.photos %>
<li class="photo" id="photo-<%= photo['id'] %>">
<a href="<%= photo['url'] %>" class="thumbnail" id="thumbnail-<%= photo['id'] %>"><img src="<%= photo['url'] %>" width="64" height="64"></a>
<br>
<a href="#" class="remove-photo" id="remove-photo-<%= photo['id'] %>">remove</a>
</li>
<% end %>
<li id="add-photo-box" class="<%= @project.count_photos >= 10 ? 'hidden' : '' %>">
<!--[if IE]>
<input type="file" id="ie-photo-uploader" style="display: none">
<br>
add photo
<![endif]-->
<![if !IE]>
<a href="#photos" class="add-photo"><img src="/images/add-photo.png" width="64" height="64"></a>
<br>
<a href="#photos" class="add-photo">add photo</a>
<![endif]>
</li>
</ul>
<p id="drag-n-drop">Drag and drop photos to change their order.</p>
</div>
<p class="save">
<input type="submit" class="button save-button" value="Save <%= @project.name.present? ? @project.name : 'This Project' %>">
<span class="save-button-spinner"><img src="/images/spinner.gif"> Saving...</span>
</p>
</form>
<script>
window.SI = window.SI || {}
window.SI.projectId = '<%= @project.id %>'
</script>

View file

@ -8,21 +8,11 @@
<p><strong>session:</strong> <%= session.inspect %></p>
<% if admin %>
<p><strong>Admin:</strong> <%= admin.name %> &lt;<%= admin.email %>&gt;</p>
<pre><%= admin.field_array %></pre>
<% end %>
<% if account %>
<p><strong>Account:</strong> <%= account.name %> &lt;<%= account.email %>&gt;</p>
<pre><%= account.field_array %></pre>
<% end %>
<% if project %>
<p><strong>Project:</strong> <%= project.id %> (<%= project.name %>)</p>
<pre><%= project.field_array %></pre>
<% end %>
<pre>
<%= error.class %>: <%= error.message %>

View file

@ -1,5 +1,5 @@
<h2 class="section-heading top">Oops</h2>
<h3 class="section-heading top">Oops</h3>
<div class="section">
<p align="center">

View file

@ -1,4 +1,4 @@
<h2 class="section-heading top">Frequently Asked Questions</h2>
<h3 class="section-heading top">Frequently Asked Questions</h3>
<div class="section">
<%== @faq %>

View file

@ -1,4 +1,4 @@
<h2 class="section-heading top">Forgot Password</h2>
<h3 class="section-heading top">Forgot Password</h3>
<%== flash_message %>

View file

@ -3,9 +3,22 @@
<head>
<meta charset="utf-8">
<meta name="description" content="Stormy Weather is bigger than The Beatles.">
<meta name="author" content="Sami Samhuri">
<meta name="description" content="My awesome site.">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= @page_title ? @page_title + ' - ' : '' %>Stormy Weather</title>
<title>Stormy Weather<%= admin_page? ? ' Admin' : '' %><%= @page_title ? ' - ' + @page_title : '' %></title>
<!-- Le HTML5 shim, for IE6-8 support of HTML elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<!-- Le fav and touch icons -->
<link rel="shortcut icon" href="images/favicon.ico">
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="72x72" href="images/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="114x114" href="images/apple-touch-icon-114x114.png">
<% for url in stylesheets %>
<link rel="stylesheet" href="<%= url %>" charset="utf-8">
@ -15,50 +28,87 @@
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="/">Stormy Weather</a>
<div class="nav-collapse">
<ul class="nav">
<li <%== request.path_info == '/' ? 'class="active"' : '' %>><a href="/">Home</a></li>
<% if admin_authorized? %>
<li <%== request.path_info == '/admin' ? 'class="active"' : '' %>><a href="/admin">Admin Dashboard</a></li>
<li <%== request.path_info == '/admin/accounts' ? 'class="active"' : '' %>><a href="/admin/accounts">Accounts: <%= num_accounts %></a></li>
<li <%== request.path_info == '/admin/faq' ? 'class="active"' : '' %>><a href="/admin/faq">FAQ</a></li>
<% end %>
</ul>
<% if authorized? || admin_authorized? %>
<p class="navbar-text pull-right">
<% if authorized? %>
Signed in as <a href="/account"><%= current_account.email %></a>
&bull;
<a href="#" id="sign-out-link">Sign Out</a>
<% end %>
<% if admin_authorized? %>
<% if authorized? %><br><% end %>
Signed in as admin: <a href="/admin/password"><%= current_account.email %></a>
&bull;
<a href="#" id="admin-sign-out-link">Sign out of admin</a>
<% end %>
</p>
<% else %>
<p class="navbar-text pull-right">
<a href="/sign-in">Sign in</a>
or
<a href="/sign-up">Sign up</a>
</p>
<% end %>
</div><!--/.nav-collapse -->
</div>
</div>
</div>
<% if authorized? %>
<form id="sign-out-form" action="/sign-out" method="post"></form>
<% end %>
<div id="wrapper">
<p id="nav" class="<%= authorized? ? 'authorized' : '' %>">
<% if authorized? %>
Signed in as <%= current_account.email %>
<br>
<a href="/projects">Projects</a>
|
<a href="/account">Account</a>
|
<a href="#" id="sign-out-link">Sign Out</a>
<% else %>
<a href="/">Home</a>
|
<a href="/sign-up">Sign Up</a>
|
<a href="/sign-in">Sign In</a>
<div class="container">
<% if breadcrumbs.size > 0 %>
<ul class="breadcrumb">
<% breadcrumbs.each_with_index do |crumb, i| %>
<li class="<%= crumb[:active] ? 'active' : '' %>">
<a href="<%= crumb[:path] %>"><%= crumb[:name] %></a>
<% if i < breadcrumbs.size - 1%>
<span class="divider">/</span>
<% end %>
</li>
<% end %>
</p>
</ul>
<% end %>
<div id="header">
<a id="logo" href="/"><img src="/images/logo.png" width="600" height="72" alt="Stormy Weather"></a>
</div>
<%== flash_message %>
<div id="content">
<%== yield %>
</div>
<%== yield %>
<div id="footer">
<p id="copyright">&copy; 2012 Sami Samhuri</p>
<hr>
<footer>
<p>&copy; 2012 Sami Samhuri</p>
<p>
<a href="/contact">About Us</a>
<a href="/contact">Contact Us</a>
&bull;
<a href="/faq">FAQ</a>
&bull;
<a href="/contact">Contact Us</a>
&bull;
<a href="/terms">Terms of Service</a>
</p>
</div>
</footer>
</div>
</div><!--/.fluid-container-->
<% for url in scripts %>
<script src="<%= url %>"></script>
@ -68,7 +118,7 @@
if (false) {
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXXXXX-X']);
_gaq.push(['_setDomainName', 'example.com']);
_gaq.push(['_setDomainName', 'example.comapult.com']);
_gaq.push(['_trackPageview']);
(function() {

View file

@ -1,15 +1,9 @@
<h2 class="section-heading top">:(</h2>
<h3 class="section-heading top">:(</h3>
<div class="section">
<p align="center">Hmmm... We don't seem to have what you're looking for.</p>
<p align="center">
<% if authorized? %>
<a href="/projects">
<% else %>
<a href="/">
<% end %>
Ok fine, I'm outta here!
</a>
<a href="/">Ok fine, I'm outta here!</a>
</p>
</div>

View file

@ -1,22 +0,0 @@
<h2 class="section-heading top">Projects</h2>
<%== flash_message %>
<% if @projects.empty? %>
<!-- no projects -->
<% else %>
<table id="projects" class="section">
<tr class="headings">
<th class="name">Name</th>
<th class="created">Created</th>
</tr>
<% first_project = true %>
<% @projects.each do |project| %>
<tr class="project <%= project.name.blank? ? 'new' : '' %> <%= first_project ? 'first' : '' %>">
<td class="name"><a href="/project/<%= project.id %>"><%= project.name %></a></td>
<td class="created"><%= format_date(Time.at(project.created_timestamp)) %></td>
</tr>
<% first_project = false %>
<% end %>
</table>
<% end %>

View file

@ -1,5 +1,5 @@
<h2 class="section-heading top">Reset My Password</h2>
<h3 class="section-heading top">Reset My Password</h3>
<%== flash_message %>

View file

@ -1,43 +1,40 @@
<%== flash_message %>
<form id="sign-in-form" action="/sign-in" method="post">
<table id="sign-in">
<div class="container">
<div class="content">
<div class="row">
<div class="sign-in-form">
<h2>Sign in</h2>
<tr>
<th><label for="email">Email</label></th>
<td><input type="email" name="email" id="email" size="30" placeholder="jane.doe@example.com" value="<%= @email %>"></td>
</tr>
<form id="sign-in-form" action="/sign-in" method="post">
<fieldset>
<div class="clearfix">
<input type="email" name="email" id="email" size="30" placeholder="Email" value="<%= @email %>">
</div>
<div class="clearfix">
<input type="password" name="password" id="password" size="30" placeholder="Password">
</div>
<div class="clearfix">
<input type="checkbox" name="remember" id="remember">
<label for="remember">Keep me signed in</label>
</div>
<div class="clearfix submit">
<button id="sign-in-button" class="btn-primary" type="submit">Sign in</button>
<span id="sign-in-spinner"><img src="/images/spinner.gif"> Signing in...</span>
</div>
</fieldset>
</form>
<tr>
<th><label for="password">Password</label></th>
<td id="password-cell">
<input type="password" name="password" id="password" size="30" placeholder="Secret">
</td>
</tr>
</div><!-- sign-in-form -->
</div><!-- row -->
</div> <!-- content -->
<tr>
<th></th>
<td>
<input type="checkbox" name="remember" id="remember">
<label for="remember">Keep me signed in</label>
</td>
</tr>
<tr>
<th></th>
<td id="submit-cell">
<input id="sign-in-button" type="submit" value="Sign In">
<span id="sign-in-spinner"><img src="/images/spinner.gif"> Signing in...</span>
</td>
</tr>
<tr>
<td colspan="2" id="sign-up">
<a href="/forgot-password" id="forgot-password-link">Forgot your password?</a>
<br>
<div class="row other-actions">
<p style="float:right">
Not a member yet? <a href="/sign-up">Sign Up!</a>
</td>
</tr>
</table>
</form>
</p>
<p>
<a href="/forgot-password" id="forgot-password-link">Forgot your password?</a>
</p>
</div><!-- row -->
</div><!-- container -->

View file

@ -1,5 +1,3 @@
<p id="blurb">Please provide the following information in order to sign up.</p>
<% if @errors %>
<script>
window.SI = window.SI || {}
@ -9,49 +7,41 @@ window.SI.errors = <%== JSON.fast_generate(@errors) %>
<%== flash_message %>
<form id="sign-up-form" action="/sign-up" method="post">
<table id="sign-up-table">
<tr>
<th><label for="first_name">First name</label></th>
<td><input type="text" id="first_name" name="first_name" size="40" placeholder="Jane" value="<%= @fields['first_name'] %>"></td>
</tr>
<tr>
<th><label for="last_name">Last name</label></th>
<td><input type="text" id="last_name" name="last_name" size="40" placeholder="Doe" value="<%= @fields['last_name'] %>"></td>
</tr>
<tr>
<th><label for="email">Email</label></th>
<td><input type="email" id="email" name="email" size="40" placeholder="jane.doe@example.com" value="<%= @fields['email'] %>"></td>
</tr>
<tr>
<th><label for="password">Password</label></th>
<td><input type="password" id="password" name="password" size="40" placeholder="Secret" value="<%= @fields['password'] %>"></td>
</tr>
<tr>
<th><label for="password_confirmation">Confirm password</label></th>
<td><input type="password" id="password_confirmation" name="password_confirmation" size="40" placeholder="Again" value="<%= @fields['password'] %>"></td>
</tr>
<tr>
<th></th>
<td id="terms-cell">
<input type="checkbox" id="terms" name="terms" <%= @fields['terms'] ? 'checked' : '' %>>
<label for="terms">Agree to the <a href="/terms">terms of service</a>.</label>
</td>
</tr>
<tr>
<th></th>
<td id="sign-up-cell">
<input id="sign-up-button" type="submit" value="SIGN UP">
<span id="sign-up-spinner"><img src="/images/spinner.gif"> Signing up...</span>
</td>
</tr>
</table>
</form>
<div class="container">
<div class="content">
<div class="row">
<div class="sign-up-form">
<h2>Sign up</h2>
<div id="sign-in">
<p>Already have an account?</p>
<a href="/sign-in" id="sign-in-button"><button>Sign In</button></a>
<img src="/images/spinner.gif" id="sign-in-spinner">
</div>
<form id="sign-up-form" action="/sign-up" method="post">
<fieldset>
<div class="clearfix">
<input type="text" id="first_name" name="first_name" size="40" placeholder="First name" value="<%= @fields['first_name'] %>">
</div>
<div class="clearfix">
<input type="text" id="last_name" name="last_name" size="40" placeholder="Last name" value="<%= @fields['last_name'] %>">
</div>
<div class="clearfix">
<input type="email" id="email" name="email" size="40" placeholder="Email" value="<%= @fields['email'] %>">
</div>
<div class="clearfix">
<input type="password" id="password" name="password" size="40" placeholder="Password" value="<%= @fields['password'] %>">
</div>
<div class="clearfix">
<input type="password" id="password_confirmation" name="password_confirmation" size="40" placeholder="Confirm password" value="<%= @fields['password'] %>">
</div>
<div class="clearfix">
<input type="checkbox" id="terms" name="terms" <%= @fields['terms'] ? 'checked' : '' %>>
<label for="terms">Agree to the <a href="/terms">terms of service</a>.</label>
</div>
<div class="clearfix submit">
<input id="sign-up-button" type="submit" class="btn-primary" value="Sign up">
<span id="sign-up-spinner"><img src="/images/spinner.gif"> Signing up...</span>
</div>
</fieldset>
</form>
<div class="clear"></div>
</div><!-- sign-up-form -->
</div><!-- row -->
</div><!-- content -->
</div><!-- container -->

View file

@ -1,10 +1,6 @@
<h2 class="section-heading top"><%= @page_title =%></h2>
<div class="section">
<h4>Legal Terms &amp; Conditions</h4>
<p>
Put yer legalese here.
</p>
<div class="container">
<div class="row">
<h1><%= @page_title %></h1>
<p>legalese goes here</p>
</div>
</div>