heroku support + fix minor bugs

This commit is contained in:
dinedine 2010-06-14 15:04:01 +02:00
parent 28211edad6
commit 29c22c05c1
21 changed files with 343 additions and 23 deletions

1
.gitignore vendored
View File

@ -15,3 +15,4 @@ pkg
rails_3_gems
doc/performance.txt
doc/production.sh
*.gem

View File

@ -20,6 +20,7 @@ gem "mimetype-fu", :require => "mimetype_fu"
gem "formtastic-rails3", :require => "formtastic"
gem "carrierwave-rails3", :require => "carrierwave"
gem "actionmailer-with-request", :require => 'actionmailer_with_request'
gem "heroku"
# Development environment
group :development do

View File

@ -5,7 +5,7 @@
= f.foldable_inputs :name => :custom_fields, :class => 'editable-list fields' do
- ordered_custom_fields.each do |field|
= f.fields_for collection_name.to_sym, field, :child_index => field._index do |g|
%li{ :class => "item added #{'error' unless field.errors.empty?}"}
%li{ :class => "item added #{'new' if f.object.new_record?} #{'error' unless field.errors.empty?}"}
%span.handle
= image_tag 'admin/form/icons/drag.png'
@ -15,7 +15,7 @@
= g.hidden_field :hint, :class => 'hint'
= g.text_field :label
= g.text_field :label, :class => 'label'
—

View File

@ -8,9 +8,9 @@
/ [if IE]
= stylesheet_link_tag 'admin/blueprint/ie', :media => 'screen'
= stylesheet_link_tag 'admin/layout', 'admin/plugins/toggle', 'admin/menu', 'admin/buttons', 'admin/formtastic', 'admin/formtastic_changes', 'admin/application', :media => 'screen'
= stylesheet_link_tag 'admin/layout', 'admin/plugins/toggle', 'admin/menu', 'admin/buttons', 'admin/formtastic', 'admin/formtastic_changes', 'admin/application', :media => 'screen', :cache => Rails.env.production? && !Locomotive.heroku?
= javascript_include_tag 'admin/jquery', 'admin/jquery.ui', 'admin/rails', 'admin/utils', 'admin/plugins/toggle', 'admin/plugins/growl', 'admin/plugins/cookie', 'admin/application'
= javascript_include_tag 'admin/jquery', 'admin/jquery.ui', 'admin/rails', 'admin/utils', 'admin/plugins/toggle', 'admin/plugins/growl', 'admin/plugins/cookie', 'admin/application', :cache => Rails.env.production? && !Locomotive.heroku?
%script{ :type => 'text/javascript' }
= find_and_preserve(growl_message)

View File

@ -4,7 +4,7 @@
%head
%title= escape_once("#{Locomotive.config.name} — #{current_site.name}")
= stylesheet_link_tag 'admin/blueprint/screen', 'admin/login', :media => 'screen'
= stylesheet_link_tag 'admin/blueprint/screen', 'admin/login', :media => 'screen', :cache => Rails.env.production? && !Locomotive.config.heroku
/ [if IE]
= stylesheet_link_tag('admin/blueprint/ie', :media => 'screen')

View File

@ -39,6 +39,8 @@ en:
string: Simple Input
text: Text
category: Select
edit_field:
title: Edit field
edit_category:
title: Edit options
help: Manage the list of options for your select box.

View File

@ -1,8 +1,7 @@
BOARD:
- deploy on Heroku
- observers to add / remove domains
- "field name" for alias / hint for custom fields
- observers to add / remove domains (http://groups.google.com/group/heroku/browse_thread/thread/148d6ea68e4574fb/4d8f1c8545d52bda?lnk=gst&q=heroku+gem+api#4d8f1c8545d52bda)
BACKLOG:
@ -23,8 +22,11 @@ BACKLOG:
- refactor slugify method (use parameterize + create a module)
- cucumber features for admin pages
- Heroku / S3 / Worker
BUGS:
- custom fields: accepts_nested_attributes weird behaviour when creating new content type + adding random fields
NICE TO HAVE:
- asset collections: custom resizing if image
@ -128,4 +130,7 @@ x upload files in S3
x [BUG] asset vignette: name + icon not vertically aligned
x truncate nom dans le menu de contents
x site subdomain regexp [a-z][A-Z][0-9]
! migrate content_instance to its own collection => http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24sliceoperator
! migrate content_instance to its own collection => http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24sliceoperator
x [BUG] "field name" for alias / hint for custom fields
x [BUG] can not remove custom fields at creation if object is invalid
x internal logger

View File

@ -9,4 +9,18 @@ Locomotive.configure do |config|
# tell if logs are enabled. Useful for debug purpose.
config.enable_logs = true
# tell if the application is hosted on Heroku.
# Locomotive uses heroku api to add / remove domains.
# there are 2 ways of passing heroku credentials to Locomotive
# - from ENV variables: HEROKU_LOGIN & HEROKU_PASSWORD
# - from this file
#
# Notes:
# - IMPORTANT: behaviours related to this option will only be applied in production
# - credentials coming from this file take precedence over ENV variables
#
# Ex:
# config.heroku = { :name => '<my heroku app name>', :login => 'john@doe.net', :password => 'easy' }
config.heroku = false
end

View File

@ -1,12 +1,16 @@
# require 'locomotive/patches'
require 'locomotive/configuration'
require 'locomotive/logger'
require 'locomotive/liquid'
require 'locomotive/mongoid'
require 'locomotive/heroku'
require 'mongo_session_store/mongoid'
module Locomotive
include Locomotive::Heroku
class << self
attr_accessor :config
@ -26,12 +30,22 @@ module Locomotive
def self.after_configure
raise '[Error] Locomotive needs a default domain name' if Locomotive.config.default_domain.blank?
ActionMailer::Base.default_url_options[:host] = Locomotive.config.default_domain + (Rails.env.development? ? ':3000' : '')
ActionMailer::Base.default_url_options[:host] = self.config.default_domain + (Rails.env.development? ? ':3000' : '')
# cookies stored in mongodb
Rails.application.config.session_store :mongoid_store, {
:key => Locomotive.config.cookie_key,
:domain => ".#{Locomotive.config.default_domain}"
}
# Heroku support
self.enable_heroku if self.heroku?
end
def self.logger(message)
if Locomotive.config.enable_logs == true
Rails.logger.info(message)
end
end
end

View File

@ -3,13 +3,14 @@ module Locomotive
@@defaults = {
:name => 'LocomotiveApp',
:default_domain => 'rails.local.fr',
:default_domain => 'example.com',
:reserved_subdomains => %w{www admin email blog webmail mail support help site sites},
# :forbidden_paths => %w{layouts snippets stylesheets javascripts assets admin system api},
:reserved_slugs => %w{stylesheets javascripts assets admin images api pages},
:locales => %w{en fr},
:cookie_key => '_locomotive_session',
:enable_logs => false
:enable_logs => false,
:heroku => false
}
cattr_accessor :settings

59
lib/locomotive/heroku.rb Normal file
View File

@ -0,0 +1,59 @@
require 'heroku'
module Locomotive
module Heroku
extend ActiveSupport::Concern
included do
class << self
attr_accessor :heroku_connection
attr_accessor :heroku_domains
end
end
module ClassMethods
def heroku?
!self.config.heroku.nil? && self.config.heroku.respond_to?(:[])
end
def enable_heroku
raise 'Heroku application name is mandatory' if self.config.heroku[:name].blank?
self.open_heroku_connection
self.enhance_site_model
# "cache" domains for better performance
self.heroku_domains = self.heroku_connection.list_domains(self.config.heroku[:name]).collect { |h| h[:domain] }
end
def open_heroku_connection
login = self.config.heroku[:login] || ENV['HEROKU_LOGIN']
password = self.config.heroku[:password] || ENV['HEROKU_PASSWORD']
self.heroku_connection = ::Heroku::Client.new(login, password)
end
def enhance_site_model
Site.send :include, Locomotive::Heroku::CustomDomain
end
# manage domains
def add_heroku_domain(name)
Locomotive.logger "[add heroku domain] #{name}"
self.heroku_connection.add_domain(self.config.heroku[:name], name)
self.heroku_domains << name
end
def remove_heroku_domain(name)
Locomotive.logger "[remove heroku domain] #{name}"
self.heroku_connection.remove_domain(self.config.heroku[:name], name)
self.heroku_domains.delete(name)
end
end
end
end

View File

@ -0,0 +1,52 @@
module Locomotive
module Heroku
module CustomDomain
extend ActiveSupport::Concern
included do
after_save :add_heroku_domains
after_destroy :remove_heroku_domains
alias_method_chain :add_subdomain_to_domains, :heroku
end
module InstanceMethods
protected
def add_subdomain_to_domains_with_heroku
unless self.domains_change.nil?
full_subdomain = "#{self.subdomain}.#{Locomotive.config.default_domain}"
@heroku_domains_change = {
:added => self.domains_change.last - self.domains_change.first - [full_subdomain],
:removed => self.domains_change.first - self.domains_change.last - [full_subdomain]
}
end
add_subdomain_to_domains_without_heroku
end
def add_heroku_domains
return if @heroku_domains_change.nil?
@heroku_domains_change[:added].each do |name|
Locomotive.add_heroku_domain(name)
end
@heroku_domains_change[:removed].each do |name|
Locomotive.remove_heroku_domain(name)
end
end
def remove_heroku_domains
self.domains_without_subdomain.each do |name|
Locomotive.remove_heroku_domain(name)
end
end
end
end
end
end

11
lib/locomotive/logger.rb Normal file
View File

@ -0,0 +1,11 @@
module Locomotive
module Logger
def self.method_missing(meth, args, &block)
if Locomotive.config.enable_logs == true
Rails.logger.send(meth, args)
end
end
end
end

View File

@ -36,7 +36,6 @@ module Locomotive
assigns = {
'site' => current_site,
'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new(current_site),
# 'theme_assets' => Locomotive::Liquid::Drops::ThemeAssets.new(current_site),
'stylesheets' => Locomotive::Liquid::Drops::Stylesheets.new(current_site),
'javascripts' => Locomotive::Liquid::Drops::Javascripts.new(current_site),
'contents' => Locomotive::Liquid::Drops::Contents.new(current_site),

View File

@ -19,7 +19,7 @@ module Locomotive
protected
def fetch_site
logger.info "[fetch site] host = #{request.host} / #{request.env['HTTP_HOST']}" if Locomotive.config.enable_logs
Locomotive.logger "[fetch site] host = #{request.host} / #{request.env['HTTP_HOST']}"
@current_site ||= Site.match_domain(request.host).first
end

View File

@ -44,6 +44,10 @@ $(document).ready(function() {
$('fieldset.fields li.template button').click(function() {
var lastRow = $(this).parents('li.template');
var label = lastRow.find('input.label').val().trim();
if (label == '' || label == defaultValue) return false;
var newRow = lastRow.clone(true).removeClass('template').addClass('added new').insertBefore(lastRow);
var dateFragment = '[' + new Date().getTime() + ']';
@ -51,10 +55,6 @@ $(document).ready(function() {
$(this).attr('name', $(this).attr('name').replace('[-1]', dateFragment));
});
// should copy the value of the select box
var input = newRow.find('input.label');
if (lastRow.find('input.label').val() == '') input.val("undefined");
var select = newRow.find('select')
.val(lastRow.find('select').val())
.change(function() { selectOnChange(select); })
@ -73,9 +73,8 @@ $(document).ready(function() {
select.show();
});
// then reset the form
lastRow.find('input').val(defaultValue).addClass('void');
lastRow.find('select').val('input');
// then "reset" the form
lastRow.find('input.label').val(defaultValue).addClass('void');
// warn the sortable widget about the new row
$("fieldset.fields ol").sortable('refresh');
@ -124,7 +123,7 @@ $(document).ready(function() {
e.stopPropagation();
});
var alias = link.parent().prevAll('.alias').val();
var alias = link.parent().prevAll('.alias').val().trim();
if (alias == '') alias = makeSlug(link.parent().prevAll('.label').val());
$('#fancybox-wrap #custom_fields_field__alias').val(alias);
@ -132,8 +131,10 @@ $(document).ready(function() {
$('#fancybox-wrap #custom_fields_field_hint').val(hint);
},
onCleanup: function() {
link.parent().prevAll('.alias').val($('#fancybox-wrap #custom_fields_field__alias').val());
link.parent().prevAll('.hint').val($('#fancybox-wrap #custom_fields_field_hint').val());
var alias = $('#fancybox-wrap #custom_fields_field__alias').val().trim();
if (alias != '') link.parent().prevAll('.alias').val(alias);
var hint = $('#fancybox-wrap #custom_fields_field_hint').val().trim();
if (hint != '') link.parent().prevAll('.hint').val(hint);
}
})
});

View File

@ -1,4 +1,5 @@
function makeSlug(val, sep) { // code largely inspired by http://www.thewebsitetailor.com/jquery-slug-plugin/
if (typeof val == 'undefined') return('');
if (typeof sep == 'undefined') sep = '_';
var alphaNumRegexp = new RegExp('[^a-zA-Z0-9\\' + sep + ']', 'g');
var avoidDuplicateRegexp = new RegExp('[\\' + sep + ']{2,}', 'g');
@ -7,3 +8,7 @@ function makeSlug(val, sep) { // code largely inspired by http://www.thewebsitet
val = val.replace(avoidDuplicateRegexp, sep);
return val.toLowerCase();
}
String.prototype.trim = function() {
return this.replace(/^\s+/g, '').replace(/\s+$/g, '');
}

View File

@ -0,0 +1,149 @@
require 'spec_helper'
describe 'Heroku support' do
before(:each) do
::Heroku::Client.any_instance.stubs(:post).returns(true)
::Heroku::Client.any_instance.stubs(:delete).returns(true)
end
context '#loaded' do
it 'has method to enable heroku' do
Locomotive.respond_to?(:enable_heroku).should be_true
end
it 'tells heroku is disabled' do
Locomotive.heroku?.should be_false
end
it 'does not add instance methods to Site' do
Site.instance_methods.include?(:add_heroku_domains).should be_false
Site.instance_methods.include?(:remove_heroku_domains).should be_false
end
end
context '#disabled' do
before(:each) do
Locomotive.configure do |config|
config.heroku = false
end
end
it 'has a nil connection' do
Locomotive.heroku_connection.should be_nil
end
it 'tells heroku is disabled' do
Locomotive.heroku?.should be_false
end
it 'does not add methods to Site' do
Site.instance_methods.include?(:add_heroku_domains).should be_false
Site.instance_methods.include?(:remove_heroku_domains).should be_false
end
end
context '#enabled' do
it 'tells heroku is disabled' do
configure_locomotive_with_heroku
Locomotive.heroku?.should be_true
end
it 'raises an exception if no app name is given' do
lambda {
configure_locomotive_with_heroku(:name => nil)
}.should raise_error
end
context 'dealing with heroku connection' do
it 'opens a heroku connection with provided credentials' do
configure_locomotive_with_heroku
Locomotive.heroku_connection.user.should == 'john@doe.net'
Locomotive.heroku_connection.password.should == 'easyone'
end
it 'opens a heroku connection with env credentials' do
ENV['HEROKU_LOGIN'] = 'john@doe.net'; ENV['HEROKU_PASSWORD'] = 'easyone'
Locomotive.configure { |config| config.heroku = true }
Locomotive.heroku_connection.user.should == 'john@doe.net'
Locomotive.heroku_connection.password.should == 'easyone'
end
end
context 'enhancing site' do
before(:each) do
configure_locomotive_with_heroku
(@site = Factory.build(:site)).stubs(:valid?).returns(true)
end
it 'calls add_heroku_domains after saving a site' do
@site.expects(:add_heroku_domains)
@site.save
end
it 'calls remove_heroku_domains after saving a site' do
@site.expects(:remove_heroku_domains)
@site.destroy
end
context 'adding domain' do
it 'does not add new domain if no delta' do
Locomotive.heroku_connection.expects(:add_domain).never
@site.save
end
it 'adds a new domain if new one' do
@site.domains = ['www.acme.fr']
Locomotive.heroku_connection.expects(:add_domain).with('locomotive', 'www.acme.fr')
@site.save
Locomotive.heroku_domains.should include('www.acme.fr')
end
end
context 'removing domain' do
it 'does not remove domain if no delta' do
Locomotive.heroku_connection.expects(:remove_domain).never
@site.destroy
end
it 'removes domains if we destroy a site' do
@site.stubs(:domains_without_subdomain).returns(['www.acme.com'])
Locomotive.heroku_connection.expects(:remove_domain).with('locomotive', 'www.acme.com')
@site.destroy
Locomotive.heroku_domains.should_not include('www.acme.com')
end
it 'removes domain if removed' do
@site.domains = ['www.acme.fr']; @site.save
@site.domains = ['www.acme.com']
Locomotive.heroku_connection.expects(:remove_domain).with('locomotive', 'www.acme.fr')
@site.save
Locomotive.heroku_domains.should_not include('www.acme.fr')
end
end
end
end
def configure_locomotive_with_heroku(options = {}, domains = nil)
::Heroku::Client.any_instance.stubs(:list_domains).with('locomotive').returns(domains || [
{ :domain => "www.acme.com" }, { :domain => "example.com" }, { :domain => "www.example.com" }
])
Locomotive.configure do |config|
config.heroku = { :name => 'locomotive', :login => 'john@doe.net', :password => 'easyone' }.merge(options)
end
end
end

View File

@ -2,6 +2,10 @@ require 'spec_helper'
describe Site do
before(:each) do
Locomotive.stubs(:add_heroku_domain).returns(true)
end
it 'should have a valid factory' do
Factory.build(:site).should be_valid
end

View File

@ -12,6 +12,7 @@ Rspec.configure do |config|
config.mock_with :mocha
config.before(:each) do
Locomotive.config.heroku = false
Mongoid.master.collections.select { |c| c.name != 'system.indexes' }.each(&:drop)
end
end

View File

@ -1,5 +1,6 @@
Locomotive.configure do |config|
config.default_domain = 'example.com'
config.enable_logs = true
end
module Locomotive