add rspec tests for the import module + debug the cms to handle correctly the default theme

This commit is contained in:
dinedine 2010-10-29 01:36:45 +02:00
parent 680a42b8f5
commit cc0b50e22e
26 changed files with 383 additions and 112 deletions

View File

@ -10,6 +10,7 @@ gem 'devise', '= 1.1.3'
gem 'mongoid', '2.0.0.beta.19'
gem 'bson_ext', '1.1.1'
gem 'locomotive_mongoid_acts_as_tree', '0.1.5.1', :require => 'mongoid_acts_as_tree'
gem 'will_paginate'
gem 'haml', '= 3.0.18'
gem 'locomotive_liquid', '2.2.2', :require => 'liquid'

View File

@ -266,3 +266,4 @@ DEPENDENCIES
rubyzip
spork
warden
will_paginate

View File

@ -13,8 +13,17 @@ module Admin
respond_to do |format|
if @content.save
format.json { render :json => { :content => @content } }
format.html do
flash[@content_type.slug.singularize] = @content.aliased_attributes
redirect_to params[:success_callback]
end
else
format.json { render :json => { :content => @content, :errors => @content.errors } }
format.html do
flash[@content_type.slug.singularize] = @content.aliased_attributes
flash[:errors] = content.errors
redirect_to params[:error_callback]
end
end
end
end

View File

@ -42,11 +42,12 @@ module Admin
@site.save
if @site.valid?
# begin
job = Locomotive::Import::Job.new(params[:zipfile], @site)
begin
job = Locomotive::Import::Job.new(params[:zipfile], @site, { :samples => true })
Delayed::Job.enqueue job, { :site => @site, :job_type => 'import' }
# rescue;
# end # not a big deal if it did not work
rescue Exception => e
logger.error "Import failed because of #{e.message}"
end
redirect_to admin_session_url(:host => Site.first.domains.first, :port => request.port)
else

View File

@ -38,6 +38,20 @@ class ContentInstance
self._visible || self._visible.nil?
end
def aliased_attributes # TODO: move it to the custom_fields gem
hash = { :created_at => self.created_at, :updated_at => self.updated_at }
self.custom_fields.each do |field|
case field.kind
when 'file' then hash[field._alias] = self.send(field._name.to_sym).url
else
hash[field._alias] = self.send(field._name.to_sym)
end
end
hash
end
def to_liquid
Locomotive::Liquid::Drops::Content.new(self)
end

View File

@ -62,7 +62,16 @@ class ContentType
(if conditions.nil? || conditions.empty?
self.contents
else
self.contents.where(conditions)
conditions_with_names = {}
conditions.each do |key, value|
# convert alias (key) to name
field = self.content_custom_fields.detect { |f| f._alias == key }
conditions_with_names[field._name.to_sym] = value
end
self.contents.where(conditions_with_names)
end).sort { |a, b| (a.send(column) || 0) <=> (b.send(column) || 0) }
end

View File

@ -5,22 +5,24 @@ BOARD:
- editable elements should wrap a tag: div, h1, ...etc (default span)
- edit images (upload new ones, ...etc) => wait for aloha or send them an email ?
- import tool:
- add samples option
- remove existing pages / contents option
- do not override existing site name
- page templatized (tied to content type)
- samples
x import tool:
x asset whitelist
x do not override existing site name
x add samples option
x content types
x asset collections
x page templatized (tied to content type)
x remove existing pages / contents option => reset
- cosmetic / ui bugs / bugs:
x segmentation with with_scope
x paginate is not working
- assets within custom contents are not deleted when the whole content type gets destroyed (after_destroy callback ?)
- increase the input field for domain names
- drag&drop for assets ('last' class issue)
- paginate is not working
- segmentation with with_scope
- asset whitelist
- api
- handle html request (for now, it's just json)
x api
x handle html request (for now, it's just json)
- refactor slugify method (use parameterize + create a module)
- [content types] the "display column" selector should not include file types
@ -28,14 +30,13 @@ BOARD:
- global regions: keyword in editable element (http://www.mongodb.org/display/DOCS/Updating)
- write my first tutorial about locomotive
- installation guide
- detect if new installation
- no-site error redirects to the first step
- steps:
0/ welcome: domains, ...etc
1/ Create account
2/ Create new site (name, subdomain)
3/ Import theme (worker or list of sites from fs)
x installation guide
x detect if new installation
x no-site error redirects to the first step
x steps:
x welcome: domains, ...etc
x Create account
x Create new site (name, subdomain) / Import theme (worker or list of sites from fs)
! add dom_id and css_class fields in page (body structure ?)

View File

@ -5,7 +5,6 @@ module CustomFields
class FileUploader < ::CarrierWave::Uploader::Base
def store_dir
puts
"sites/#{model.site_id}/contents/#{model.class.model_name.underscore}/#{model.id}/files"
end

View File

@ -16,6 +16,7 @@ require 'actionmailer_with_request'
require 'zip/zipfilesystem'
require 'custom_fields'
require 'delayed_job_mongoid'
require 'will_paginate'
$:.unshift File.dirname(__FILE__)

View File

@ -1,3 +1,5 @@
require 'locomotive/import/logger'
require 'locomotive/import/base'
require 'locomotive/import/job'
require 'locomotive/import/site'
require 'locomotive/import/assets'

View File

@ -1,30 +1,34 @@
module Locomotive
module Import
module AssetCollections
def self.process(context)
site, database = context[:site], context[:database]
class AssetCollections < Base
def process
asset_collections = database['site']['asset_collections']
return if asset_collections.nil?
asset_collections.each do |name, attributes|
puts "....asset_collection = #{attributes['slug']}"
self.log "slug = #{attributes['slug']}"
asset_collection = site.asset_collections.where(:slug => attributes['slug']).first
asset_collection ||= self.build_asset_collection(site, attributes.merge(:name => name))
asset_collection ||= self.build_asset_collection(attributes.merge(:name => name))
self.add_or_update_fields(asset_collection, attributes['fields'])
if options[:samples] && attributes['assets']
self.insert_samples(asset_collection, attributes['assets'].symbolize_keys)
end
asset_collection.save!
site.reload
end
end
def self.build_asset_collection(site, data)
protected
def build_asset_collection(data)
attributes = { :internal => false }.merge(data)
attributes.delete_if { |name, value| %w{fields assets}.include?(name) }
@ -32,7 +36,7 @@ module Locomotive
site.asset_collections.build(attributes)
end
def self.add_or_update_fields(asset_collection, fields)
def add_or_update_fields(asset_collection, fields)
fields.each_with_index do |data, position|
name, data = data.keys.first, data.values.first
@ -48,6 +52,34 @@ module Locomotive
end
end
def insert_samples(asset_collection, assets)
assets.each_with_index do |data, position|
value, attributes = data.is_a?(Array) ? [data.first, data.last] : [data.keys.first, data.values.first]
url = attributes.delete('url')
# build with default attributes
asset = asset_collection.assets.build(:name => value, :position => position, :url => self.open_sample_asset(url))
attributes.each do |name, value|
field = asset_collection.asset_custom_fields.detect { |f| f._alias == name }
value = (case field.kind.downcase
when 'file' then self.open_sample_asset(value)
when 'boolean' then Boolean.set(value)
else
value
end)
asset.send("#{name}=", value)
end
asset.save
self.log "insert asset '#{name}'"
end
end
end
end
end

View File

@ -1,24 +1,26 @@
module Locomotive
module Import
module Assets
class Assets < Base
def self.process(context)
site, theme_path = context[:site], context[:theme_path]
def process
whitelist = self.build_regexps_in_withlist(database['site']['assets']['whitelist']) rescue nil
whitelist = self.build_regexps_in_withlist(context[:database]['site']['assets']['whitelist']) rescue nil
self.log "white list = #{whitelist.inspect}"
self.add_theme_assets(site, theme_path, whitelist)
self.add_theme_assets(whitelist)
self.add_other_assets(site, theme_path)
self.add_other_assets
end
def self.add_theme_assets(site, theme_path, whitelist)
protected
def add_theme_assets(whitelist)
%w(images media fonts javascripts stylesheets).each do |kind|
Dir[File.join(theme_path, 'public', kind, '**/*')].each do |asset_path|
next if File.directory?(asset_path)
visible = self.check_against_whitelist(whitelist, asset_path.gsub(File.join(theme_path, 'public'), ''))
visible = self.check_against_whitelist(whitelist, asset_path.gsub(File.join(theme_path, 'public'), '').gsub(/^\//, ''))
folder = asset_path.gsub(File.join(theme_path, 'public'), '').gsub(File.basename(asset_path), '').gsub(/^\//, '').gsub(/\/$/, '')
@ -31,7 +33,7 @@ module Locomotive
begin
asset.save!
rescue Exception => e
puts "\t\t !!! asset_path = #{asset_path}, folder = #{folder}, error = #{e.message}"
self.log "!ERROR! = #{e.message}, #{asset_path}"
end
site.reload
@ -39,7 +41,7 @@ module Locomotive
end
end
def self.add_other_assets(site, theme_path)
def add_other_assets
collection = AssetCollection.find_or_create_internal(site)
Dir[File.join(theme_path, 'public', 'samples', '*')].each do |asset_path|
@ -52,7 +54,7 @@ module Locomotive
end
end
def self.build_regexps_in_withlist(rules)
def build_regexps_in_withlist(rules)
rules.collect do |rule|
if rule.start_with?('^')
Regexp.new(rule.gsub('/', '\/'))
@ -62,7 +64,7 @@ module Locomotive
end
end
def self.check_against_whitelist(whitelist, path)
def check_against_whitelist(whitelist, path)
(whitelist || []).each do |rule|
case rule
when Regexp

View File

@ -0,0 +1,46 @@
module Locomotive
module Import
class Base
include Logger
attr_reader :context, :options
def initialize(context, options)
@context = context
@options = options
self.log "*** starting to process ***"
end
def self.process(context, options)
self.new(context, options).process
end
def process
raise 'this method has to be overidden'
end
def log(message)
super(message, self.class.name.demodulize.underscore)
end
protected
def site
@context[:site]
end
def database
@context[:database]
end
def theme_path
@context[:theme_path]
end
def open_sample_asset(url)
File.open(File.join(self.theme_path, 'public', url))
end
end
end
end

View File

@ -1,20 +1,16 @@
module Locomotive
module Import
module ContentTypes
def self.process(context)
site, database = context[:site], context[:database]
content_types = database['site']['content_types']
class ContentTypes < Base
def process
return if content_types.nil?
content_types.each do |name, attributes|
puts "\t\t....content_type = #{attributes['slug']}"
self.log "[content_types] slug = #{attributes['slug']}"
content_type = site.content_types.where(:slug => attributes['slug']).first
content_type ||= self.build_content_type(site, attributes.merge(:name => name))
content_type ||= self.build_content_type(attributes.merge(:name => name))
self.add_or_update_fields(content_type, attributes['fields'])
@ -24,13 +20,23 @@ module Locomotive
self.set_group_by_value(content_type)
if options[:samples] && attributes['contents']
self.insert_samples(content_type, attributes['contents'])
end
content_type.save!
site.reload
end
end
def self.build_content_type(site, data)
protected
def content_types
database['site']['content_types']
end
def build_content_type(data)
attributes = { :order_by => '_position_in_list', :group_by_field_name => data.delete('group_by') }.merge(data)
attributes.delete_if { |name, value| %w{fields contents}.include?(name) }
@ -38,7 +44,7 @@ module Locomotive
site.content_types.build(attributes)
end
def self.add_or_update_fields(content_type, fields)
def add_or_update_fields(content_type, fields)
fields.each_with_index do |data, position|
name, data = data.keys.first, data.values.first
@ -54,13 +60,39 @@ module Locomotive
end
end
def self.set_highlighted_field_name(content_type)
def insert_samples(content_type, contents)
contents.each_with_index do |data, position|
value, attributes = data.is_a?(Array) ? [data.first, data.last] : [data.keys.first, data.values.first]
# build with default attributes
content = content_type.contents.build(content_type.highlighted_field_name.to_sym => value, :_position_in_list => position)
attributes.each do |name, value|
field = content_type.content_custom_fields.detect { |f| f._alias == name }
value = (case field.kind.downcase
when 'file' then self.open_sample_asset(value)
when 'boolean' then Boolean.set(value)
else
value
end)
content.send("#{name}=", value)
end
content.save
self.log "insert content '#{value}'"
end
end
def set_highlighted_field_name(content_type)
field = content_type.content_custom_fields.detect { |f| f._alias == content_type.highlighted_field_name }
content_type.highlighted_field_name = field._name if field
end
def self.set_order_by_value(content_type)
def set_order_by_value(content_type)
order_by = (case content_type.order_by
when 'manually', '_position_in_list' then '_position_in_list'
when 'date', 'updated_at' then 'updated_at'
@ -71,7 +103,7 @@ module Locomotive
content_type.order_by = order_by || '_position_in_list'
end
def self.set_group_by_value(content_type)
def set_group_by_value(content_type)
return if content_type.group_by_field_name.blank?
field = content_type.content_custom_fields.detect { |f| f._alias == content_type.group_by_field_name }

View File

@ -4,9 +4,15 @@ module Locomotive
module Import
class Job
def initialize(zipfile, site, enabled = {})
include Logger
def initialize(zipfile, site, options = {})
@site = site
@enabled = enabled
@options = {
:reset => false,
:samples => false,
:enabled => {}
}.merge(options)
@identifier = self.store_zipfile(zipfile)
@ -18,7 +24,7 @@ module Locomotive
end
def perform
self.log "theme identifier #{@identifier} / enabled steps = #{@enabled.inspect}"
self.log "theme identifier #{@identifier}"
self.unzip!
@ -32,10 +38,11 @@ module Locomotive
:worker => @worker
}
self.reset! if @options[:reset]
%w(site content_types assets asset_collections snippets pages).each do |step|
if @enabled[step] != false
self.log "performing '#{step}' step"
"Locomotive::Import::#{step.camelize}".constantize.process(context)
if @options[:enabled][step] != false
"Locomotive::Import::#{step.camelize}".constantize.process(context, @options)
@worker.update_attributes :step => step if @worker
else
self.log "skipping #{step}"
@ -59,10 +66,6 @@ module Locomotive
protected
def log(message)
puts "\t[import_theme] #{message}"
end
def themes_folder
File.join(Rails.root, 'tmp', 'themes', @site.id.to_s)
end
@ -136,6 +139,13 @@ module Locomotive
end
def reset!
@site.pages.destroy_all
@site.theme_assets.destroy_all
@site.content_types.destroy_all
@site.asset_collections.destroy_all
end
end
end
end

View File

@ -0,0 +1,13 @@
module Locomotive
module Import
module Logger
def log(message, domain = '')
head = "[import_theme]"
head += "[#{domain}]" unless domain.blank?
::Locomotive::Logger.info "\t#{head} #{message}"
end
end
end
end

View File

@ -1,13 +1,11 @@
module Locomotive
module Import
module Pages
def self.process(context)
site, pages, theme_path = context[:site], context[:database]['pages'], context[:theme_path]
class Pages < Base
def process
context[:done] = {} # initialize the hash storing pages already processed
self.add_index_and_404(context)
self.add_index_and_404
Dir[File.join(theme_path, 'templates', '**/*')].each do |template_path|
@ -15,43 +13,47 @@ module Locomotive
next if %w(index 404).include?(fullpath)
self.add_page(fullpath, context)
self.add_page(fullpath)
end
end
def self.add_page(fullpath, context)
puts "\t\t....adding #{fullpath}"
protected
def add_page(fullpath)
page = context[:done][fullpath]
return page if page # already added, so skip it
site, pages, theme_path = context[:site], context[:database]['site']['pages'], context[:theme_path]
template = File.read(File.join(theme_path, 'templates', "#{fullpath}.liquid")) rescue "Unable to find #{fullpath}.liquid"
self.build_parent_template(template, context)
self.build_parent_template(template)
parent = self.find_parent(fullpath, context)
page = site.pages.where(:fullpath => fullpath).first || site.pages.build
parent = self.find_parent(fullpath)
attributes = {
:title => fullpath.split('/').last.humanize,
:slug => fullpath.split('/').last,
:parent => parent,
:raw_template => template
}.merge(pages[fullpath] || {}).symbolize_keys
}.merge(self.pages[fullpath] || {}).symbolize_keys
# templatized ?
if content_type_slug = attributes.delete(:content_type)
attributes[:content_type] = site.content_types.where(:slug => content_type_slug).first
fullpath.gsub!(/\/template$/, '/content_type_template')
attributes.merge!({
:templatized => true,
:content_type => site.content_types.where(:slug => content_type_slug).first
})
end
page = site.pages.where(:fullpath => fullpath).first || site.pages.build
page.attributes = attributes
page.save!
self.log "adding #{page.fullpath}"
site.reload
context[:done][fullpath] = page
@ -59,7 +61,7 @@ module Locomotive
page
end
def self.build_parent_template(template, context)
def build_parent_template(template)
# just check if the template contains the extends keyword
fullpath = template.scan(/\{% extends (\w+) %\}/).flatten.first
@ -68,13 +70,11 @@ module Locomotive
return if fullpath == 'parent'
self.add_page(fullpath, context)
self.add_page(fullpath)
end
end
def self.find_parent(fullpath, context)
site = context[:site]
def find_parent(fullpath)
segments = fullpath.split('/')
return site.pages.index.first if segments.size == 1
@ -86,12 +86,10 @@ module Locomotive
# look for a local index page in db
parent = site.pages.where(:fullpath => parent_fullpath).first
parent || self.add_page(parent_fullpath, context)
parent || self.add_page(parent_fullpath)
end
def self.add_index_and_404(context)
site, pages, theme_path = context[:site], context[:database]['site']['pages'], context[:theme_path]
def add_index_and_404
%w(index 404).each_with_index do |slug, position|
page = site.pages.where({ :slug => slug, :depth => 0 }).first
@ -99,7 +97,7 @@ module Locomotive
template = File.read(File.join(theme_path, 'templates', "#{slug}.liquid"))
page.attributes = { :raw_template => template, :position => position }.merge(pages[slug] || {})
page.attributes = { :raw_template => template, :position => position }.merge(self.pages[slug] || {})
page.save! rescue nil # TODO better error handling
@ -109,6 +107,10 @@ module Locomotive
end
end
def pages
context[:database]['site']['pages']
end
end
end
end

View File

@ -1,11 +1,9 @@
module Locomotive
module Import
module Site
class Site < Base
def self.process(context)
site, database = context[:site], context[:database]
attributes = database['site'].clone.delete_if { |name, value| %w{pages assets content_types asset_collections}.include?(name) }
def process
attributes = database['site'].clone.delete_if { |name, value| %w{name pages assets content_types asset_collections}.include?(name) }
site.attributes = attributes

View File

@ -1,20 +1,18 @@
module Locomotive
module Import
module Snippets
def self.process(context)
site, theme_path = context[:site], context[:theme_path]
class Snippets < Base
def process
Dir[File.join(theme_path, 'snippets', '*')].each do |snippet_path|
self.log "path = #{snippet_path}"
name = File.basename(snippet_path, File.extname(snippet_path)).parameterize('_')
snippet = site.snippets.where(:slug => name).first || site.snippets.build(:name => name)
snippet.template = File.read(snippet_path) # = site.snippets.create! :name => name, :template =>
snippet.template = File.read(snippet_path) # = site.snippets.create! :name => name, :template =>
snippet.save!
# puts "snippet = #{snippet.inspect}"
end
end

View File

@ -45,7 +45,7 @@ module Locomotive
protected
def paginate(options = {})
@collection ||= self.collection.paginate(options)
@collection = self.collection.paginate(options)
{
:collection => @collection,
:current_page => @collection.current_page,
@ -58,7 +58,7 @@ module Locomotive
end
def collection
@collection ||= @content_type.ordered_contents(@context['with_scope']) # remove per_page, page keys
@collection ||= @content_type.ordered_contents(@context['with_scope'])
end
end
end

View File

@ -60,7 +60,7 @@ module Locomotive
'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new,
'contents' => Locomotive::Liquid::Drops::Contents.new,
'current_page' => self.params[:page]
}
}.merge(flash.stringify_keys) # data from api
if @page.templatized? # add instance from content type
assigns['content_instance'] = @content_instance
@ -79,6 +79,8 @@ module Locomotive
end
def prepare_and_set_response(output)
flash.discard
response.headers['Content-Type'] = 'text/html; charset=utf-8'
if @page.with_cache?

View File

@ -39,6 +39,7 @@ Gem::Specification.new do |s|
s.add_dependency "delayed_job_mongoid", "1.0.0.rc"
s.add_dependency "custom_fields", "1.0.0.beta2"
s.add_dependency "rubyzip"
s.add_dependency "will_paginate"
s.files = Dir[ "Gemfile",
"{app}/**/*",

BIN
spec/fixtures/themes/default.zip vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,76 @@
require 'spec_helper'
describe Locomotive::Import::Job do
context 'when successful' do
before(:all) do
@site = Factory(:site)
job = Locomotive::Import::Job.new(FixturedTheme.duplicate_and_open('default.zip'), @site, { :samples => true, :reset => true })
job.perform
job.success nil
end
it 'updates the site information' do
@site.name.should_not == "HTML5 portfolio"
@site.meta_keywords.should == "html5 portfolio theme locomotive cms"
@site.meta_description.should == "portfolio powered by html5"
end
it 'adds content types' do
@site.content_types.count.should == 2
content_type = @site.content_types.where(:slug => 'projects').first
content_type.content_custom_fields.size.should == 6
end
it 'converts correctly the order_by option for content types' do
content_type = @site.content_types.where(:slug => 'messages').first
content_type.order_by.should == 'updated_at'
end
it 'adds samples coming with content types' do
content_type = @site.content_types.where(:slug => 'projects').first
content_type.contents.size.should == 5
content = content_type.contents.first
content.name.should == 'Locomotive App'
content.thumbnail.url.should_not be_nil
content.featured.should == true
end
it 'inserts theme assets' do
@site.theme_assets.count.should == 10
end
it 'hides some theme assets' do
asset = @site.theme_assets.where(:local_path => 'stylesheets/style.css').first
asset.hidden.should == false
asset = @site.theme_assets.where(:local_path => 'stylesheets/ie7.css').first
asset.hidden.should == true
end
it 'inserts all the pages' do
@site.pages.count.should == 8
end
it 'inserts the index and 404 pages' do
@site.pages.index.first.should_not be_nil
@site.pages.not_found.first.should_not be_nil
end
it 'inserts templatized page' do
page = @site.pages.where(:templatized => true).first
page.should_not be_nil
page.fullpath.should == 'portfolio/content_type_template'
end
after(:all) do
Site.destroy_all
end
end
end

View File

@ -21,6 +21,8 @@ Rspec.configure do |config|
DatabaseCleaner.orm = "mongoid"
end
config.before(:each) do
DatabaseCleaner.clean
if self.described_class != Locomotive::Import::Job
DatabaseCleaner.clean
end
end
end

View File

@ -5,7 +5,6 @@ CarrierWave.configure do |config|
config.store_dir = "spec/tmp/uploads"
config.cache_dir = "spec/tmp/cache"
config.root = File.join(Rails.root, 'spec', 'tmp')
# config.enable_processing = false
end
module FixturedAsset
@ -29,4 +28,24 @@ module FixturedAsset
end
end
module FixturedTheme
def self.open(filename)
File.new(self.path(filename))
end
def self.path(filename)
File.join(File.dirname(__FILE__), '..', 'fixtures', 'themes', filename)
end
def self.duplicate(filename)
dst = File.join(File.dirname(__FILE__), '..', 'tmp', filename)
FileUtils.cp self.path(filename), dst
dst
end
def self.duplicate_and_open(filename)
File.open(self.duplicate(filename))
end
end
FixturedAsset.reset!