diff --git a/app/models/asset.rb b/app/models/asset.rb index d2fcc646..ff1fe405 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -3,12 +3,7 @@ class Asset include Mongoid::Document include Mongoid::Timestamps - ## Extensions ## - include Extensions::Asset::Vignette - include CustomFields::ProxyClassEnabler - ## fields ## - field :name, :type => String field :content_type, :type => String field :width, :type => Integer field :height, :type => Integer @@ -17,10 +12,10 @@ class Asset mount_uploader :source, AssetUploader ## associations ## - embedded_in :collection, :class_name => 'AssetCollection', :inverse_of => :assets + referenced_in :site ## validations ## - validates_presence_of :name, :source + validates_presence_of :source ## behaviours ## @@ -37,10 +32,6 @@ class Asset File.extname(self.source_filename).gsub(/^\./, '') end - def site_id # needed by the uploader of custom fields - self.collection.site_id - end - def to_liquid Locomotive::Liquid::Drops::Asset.new(self) end diff --git a/app/models/asset_collection.rb b/app/models/asset_collection.rb deleted file mode 100644 index da004d60..00000000 --- a/app/models/asset_collection.rb +++ /dev/null @@ -1,81 +0,0 @@ -class AssetCollection - - include Locomotive::Mongoid::Document - - ## fields ## - field :name - field :slug - field :internal, :type => Boolean, :default => false - - ## associations ## - referenced_in :site - embeds_many :assets, :validate => false - - ## behaviours ## - custom_fields_for :assets - liquid_methods :name, :ordered_assets - - ## callbacks ## - before_validation :normalize_slug - before_save :store_asset_positions! - after_destroy :remove_uploaded_files - - ## validations ## - validates_presence_of :site, :name, :slug - validates_uniqueness_of :slug, :scope => :site_id - - ## named scopes ## - scope :internal, :where => { :internal => true } - scope :not_internal, :where => { :internal => false } - - ## methods ## - - def ordered_assets - self.assets.sort { |a, b| (a.position || 0) <=> (b.position || 0) } - end - - def assets_order - self.ordered_assets.collect(&:id).join(',') - end - - def assets_order=(order) - @assets_order = order - end - - def self.find_or_create_internal(site) - site.asset_collections.internal.first || site.asset_collections.create(:name => 'system', :slug => 'system', :internal => true) - end - - protected - - def normalize_slug - self.slug = self.name.clone if self.slug.blank? && self.name.present? - self.slug.slugify! if self.slug.present? - end - - def store_asset_positions! - return if @assets_order.nil? - - ids = @assets_order.split(',').collect { |id| BSON::ObjectId(id) } - - ids.each_with_index do |asset_id, index| - self.assets.find(asset_id).position = index - end - - self.assets.clone.each do |asset| - if !ids.include?(asset._id) - self.assets.delete(asset) - asset.send(:delete) - end - end - end - - def remove_uploaded_files # callbacks are not called on each asset so we do it manually - self.assets.each do |asset| - self.asset_custom_fields.each do |field| - asset.send(:"remove_#{field._name}!") if field.kind == 'file' - end - end - end - -end diff --git a/app/models/site.rb b/app/models/site.rb index 7873c6d5..724734c9 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -15,7 +15,7 @@ class Site references_many :pages, :validate => false references_many :snippets, :dependent => :destroy, :validate => false references_many :theme_assets, :dependent => :destroy, :validate => false - references_many :asset_collections, :dependent => :destroy, :validate => false + references_many :assets, :dependent => :destroy, :validate => false references_many :content_types, :dependent => :destroy, :validate => false embeds_many :memberships diff --git a/app/uploaders/asset_uploader.rb b/app/uploaders/asset_uploader.rb index c515da41..041cca08 100644 --- a/app/uploaders/asset_uploader.rb +++ b/app/uploaders/asset_uploader.rb @@ -2,26 +2,10 @@ class AssetUploader < CarrierWave::Uploader::Base - include CarrierWave::RMagick include Locomotive::CarrierWave::Uploader::Asset - version :thumb, :if => :image? do - process :resize_to_fill => [50, 50] - process :convert => 'png' - end - - version :medium, :if => :image? do - process :resize_to_fill => [80, 80] - process :convert => 'png' - end - - version :preview, :if => :image? do - process :resize_to_fit => [880, 1100] - process :convert => 'png' - end - def store_dir - self.build_store_dir('sites', model.collection.site_id, 'assets', model.id) + self.build_store_dir('sites', model.site_id, 'assets', model.id) end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 02694487..bf677a27 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -4,7 +4,7 @@ Locomotive::Application.configure do # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the webserver when you make code changes. - config.cache_classes = false + config.cache_classes = true # Log error messages when you accidentally call methods on nil. config.whiny_nils = true diff --git a/doc/TODO b/doc/TODO index 0c45ca12..e2a5b980 100644 --- a/doc/TODO +++ b/doc/TODO @@ -1,8 +1,18 @@ BOARD: +x bushido version + x default template ~ editable_elements: inheritable: false (Mattias) => seems to be fixed by Dirk's last pull request (#44) => content tag -- bushido version - - default template +- remove asset_collections + x site templates + - helpers + - tinyMCE plugin + - vignette.rb + - code + - ui + - rake task + - internal collection + - assign same _id BACKLOG: diff --git a/lib/locomotive.rb b/lib/locomotive.rb index 6d49cb32..be35ce3d 100644 --- a/lib/locomotive.rb +++ b/lib/locomotive.rb @@ -76,7 +76,7 @@ module Locomotive # Load all the dynamic classes (custom fields) begin ContentType.all.collect(&:fetch_content_klass) - AssetCollection.all.collect(&:fetch_asset_klass) + # AssetCollection.all.collect(&:fetch_asset_klass) rescue ::Mongoid::Errors::InvalidDatabase => e # let assume it's because of the first install (meaning no config.yml file) end diff --git a/lib/locomotive/import/asset_collections.rb b/lib/locomotive/import/asset_collections.rb index 1372e0aa..1253e80e 100644 --- a/lib/locomotive/import/asset_collections.rb +++ b/lib/locomotive/import/asset_collections.rb @@ -1,87 +1,87 @@ -module Locomotive - module Import - class AssetCollections < Base - - def process - asset_collections = database['site']['asset_collections'] - - return if asset_collections.nil? - - asset_collections.each do |name, attributes| - self.log "slug = #{attributes['slug']}" - - asset_collection = site.asset_collections.where(:slug => attributes['slug']).first - - 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']) - end - - asset_collection.save! - - site.reload - end - end - - protected - - def build_asset_collection(data) - attributes = { :internal => false }.merge(data) - - attributes.delete_if { |name, value| %w{fields assets}.include?(name) } - - site.asset_collections.build(attributes) - end - - def add_or_update_fields(asset_collection, fields) - return if fields.blank? - - fields.each_with_index do |data, position| - name, data = data.keys.first, data.values.first - - attributes = { :_alias => name, :label => name.humanize, :kind => 'string', :position => position }.merge(data).symbolize_keys - - field = asset_collection.asset_custom_fields.detect { |f| f._alias == attributes[:_alias] } - - field ||= asset_collection.asset_custom_fields.build(attributes) - - field.send(:set_unique_name!) if field.new_record? - - field.attributes = attributes - 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, :source => 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 '#{asset.name}'" - end - end - - end - end -end \ No newline at end of file +# module Locomotive +# module Import +# class AssetCollections < Base +# +# def process +# asset_collections = database['site']['asset_collections'] +# +# return if asset_collections.nil? +# +# asset_collections.each do |name, attributes| +# self.log "slug = #{attributes['slug']}" +# +# asset_collection = site.asset_collections.where(:slug => attributes['slug']).first +# +# 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']) +# end +# +# asset_collection.save! +# +# site.reload +# end +# end +# +# protected +# +# def build_asset_collection(data) +# attributes = { :internal => false }.merge(data) +# +# attributes.delete_if { |name, value| %w{fields assets}.include?(name) } +# +# site.asset_collections.build(attributes) +# end +# +# def add_or_update_fields(asset_collection, fields) +# return if fields.blank? +# +# fields.each_with_index do |data, position| +# name, data = data.keys.first, data.values.first +# +# attributes = { :_alias => name, :label => name.humanize, :kind => 'string', :position => position }.merge(data).symbolize_keys +# +# field = asset_collection.asset_custom_fields.detect { |f| f._alias == attributes[:_alias] } +# +# field ||= asset_collection.asset_custom_fields.build(attributes) +# +# field.send(:set_unique_name!) if field.new_record? +# +# field.attributes = attributes +# 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, :source => 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 '#{asset.name}'" +# end +# end +# +# end +# end +# end \ No newline at end of file diff --git a/lib/locomotive/import/content_types.rb b/lib/locomotive/import/content_types.rb index 0665671e..7355126d 100644 --- a/lib/locomotive/import/content_types.rb +++ b/lib/locomotive/import/content_types.rb @@ -1,3 +1,5 @@ +require 'ostruct' + module Locomotive module Import class ContentTypes < Base @@ -5,6 +7,8 @@ module Locomotive def process return if content_types.nil? + contents_with_associations, content_types_with_associations = [], [] + content_types.each do |name, attributes| self.log "[content_types] slug = #{attributes['slug']}" @@ -14,6 +18,10 @@ module Locomotive self.add_or_update_fields(content_type, attributes['fields']) + if content_type.content_custom_fields.any? { |f| ['has_many', 'has_one'].include?(f.kind) } + content_types_with_associations << content_type + end + self.set_highlighted_field_name(content_type) self.set_order_by_value(content_type) @@ -21,17 +29,21 @@ module Locomotive self.set_group_by_value(content_type) if options[:samples] && attributes['contents'] - self.insert_samples(content_type, attributes['contents']) + contents_with_associations += self.insert_samples(content_type, attributes['contents']) end content_type.save! - - site.reload end + # look up for associations and replace their target field by the real class name + self.replace_target(content_types_with_associations) + + # update all the contents with associations now that every content is stored in mongodb + self.insert_samples_with_associations(contents_with_associations) + # invalidate the cache of the dynamic classes (custom fields) - ContentType.all.collect(&:invalidate_content_klass) - AssetCollection.all.collect(&:invalidate_asset_klass) + site.content_types.all.collect { |c| c.invalidate_content_klass; c.fetch_content_klass } + site.asset_collections.all.collect { |c| c.invalidate_content_klass; c.fetch_content_klass } end protected @@ -61,13 +73,33 @@ module Locomotive field.send(:set_unique_name!) if field.new_record? field.attributes = attributes + + field[:kind] = field[:kind].downcase # old versions of the kind are capitalized + end + end + + def replace_target(content_types) + content_types.each do |content_type| + content_type.content_custom_fields.each do |field| + next unless ['has_many', 'has_one'].include?(field.kind) + + target_content_type = site.content_types.where(:name => field.target).first + + field.target = target_content_type.content_klass.to_s if target_content_type + end + + content_type.save end end def insert_samples(content_type, contents) + contents_with_associations = [] + contents.each_with_index do |data, position| value, attributes = data.is_a?(Array) ? [data.first, data.last] : [data.keys.first, data.values.first] + associations = [] + # build with default attributes content = content_type.contents.build(content_type.highlighted_field_name.to_sym => value, :_position_in_list => position) @@ -76,7 +108,14 @@ module Locomotive next if field.nil? # the attribute name is not related to a field (name misspelled ?) - value = (case field.kind.downcase + kind = field.kind + + if ['has_many', 'has_one'].include?(kind) + associations << OpenStruct.new(:name => name, :kind => kind, :value => value, :target => field.target) + next + end + + value = (case kind when 'file' then self.open_sample_asset(value) when 'boolean' then Boolean.set(value) when 'date' then value.is_a?(Date) ? value : Date.parse(value) @@ -85,17 +124,50 @@ module Locomotive field.category_items.build :name => value end value - else + else # string, text value end) content.send("#{name}=", value) end - content.save + content.save(:validate => false) + + contents_with_associations << [content, associations] unless associations.empty? self.log "insert content '#{content.send(content_type.highlighted_field_name.to_sym)}'" end + + contents_with_associations + end + + def insert_samples_with_associations(contents) + contents.each do |content_information| + next if content_information.empty? + + content, associations = content_information + + content = content._parent.reload.contents.find(content._id) # target should be updated + + associations.each do |association| + target_content_type = site.content_types.where(:name => association.target).first + + next if target_content_type.nil? + + value = (case association.kind + when 'has_one' then + target_content_type.contents.detect { |c| c.highlighted_field_value == association.value } + when 'has_many' then + association.value.collect do |v| + target_content_type.contents.detect { |c| c.highlighted_field_value == v }._id + end + end) + + content.send("#{association.name}=", value) + end + + content.save + end end def set_highlighted_field_name(content_type) diff --git a/lib/locomotive/import/job.rb b/lib/locomotive/import/job.rb index 5be365be..d5ab2ec0 100644 --- a/lib/locomotive/import/job.rb +++ b/lib/locomotive/import/job.rb @@ -30,7 +30,7 @@ module Locomotive self.unzip! - raise "No database.yml found in the theme zipfile" if @database.nil? + raise "No config/compiled_site.yml found in the theme zipfile" if @database.nil? context = { :database => @database, @@ -42,7 +42,7 @@ module Locomotive self.reset! if @options[:reset] - %w(site content_types assets asset_collections snippets pages).each do |step| + %w(site content_types assets snippets pages).each do |step| if @options[:enabled][step] != false "Locomotive::Import::#{step.camelize}".constantize.process(context, @options) @worker.update_attributes :step => step if @worker @@ -145,10 +145,10 @@ module Locomotive zipfile.each do |entry| next if entry.name =~ /__MACOSX/ - if entry.name =~ /database.yml$/ + if entry.name =~ /config\/compiled_site.yml$/ @database = YAML.load(zipfile.read(entry.name)) - @theme_path = File.join(destination_path, entry.name.gsub('database.yml', '')) + @theme_path = File.join(destination_path, entry.name.gsub('config/compiled_site.yml', '')) next end diff --git a/lib/locomotive/import/pages.rb b/lib/locomotive/import/pages.rb index 3647af7b..4ebce0eb 100644 --- a/lib/locomotive/import/pages.rb +++ b/lib/locomotive/import/pages.rb @@ -9,9 +9,9 @@ module Locomotive self.add_page('index') - Dir[File.join(theme_path, 'templates', '**/*')].each do |template_path| + Dir[File.join(theme_path, 'app', 'views', 'pages', '**/*')].each do |template_path| - fullpath = template_path.gsub(File.join(theme_path, 'templates'), '').gsub('.liquid', '').gsub(/^\//, '') + fullpath = template_path.gsub(File.join(theme_path, 'app', 'views', 'pages'), '').gsub('.liquid', '').gsub(/^\//, '') next if %w(index 404).include?(fullpath) @@ -37,7 +37,7 @@ module Locomotive return page if page # already added, so skip it - template = File.read(File.join(theme_path, 'templates', "#{fullpath}.liquid")) rescue "Unable to find #{fullpath}.liquid" + template = File.read(File.join(theme_path, 'app', 'views', 'pages', "#{fullpath}.liquid")) rescue "Unable to find #{fullpath}.liquid" self.replace_images!(template) @@ -146,8 +146,6 @@ module Locomotive fullpath = data.keys.first.to_s unless %w(index 404).include?(fullpath) - # position = fullpath == 'index' ? 0 : 1 - # else (segments = fullpath.split('/')).pop position_key = segments.empty? ? 'index' : segments.join('/') diff --git a/lib/locomotive/import/snippets.rb b/lib/locomotive/import/snippets.rb index abab4f2f..2208449f 100644 --- a/lib/locomotive/import/snippets.rb +++ b/lib/locomotive/import/snippets.rb @@ -3,14 +3,14 @@ module Locomotive class Snippets < Base def process - Dir[File.join(theme_path, 'snippets', '*')].each do |snippet_path| + Dir[File.join(theme_path, 'app', 'views', '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) snippet.save! end diff --git a/lib/tasks/locomotive.rake b/lib/tasks/locomotive.rake index 227de113..18b5a3a3 100644 --- a/lib/tasks/locomotive.rake +++ b/lib/tasks/locomotive.rake @@ -1,3 +1,7 @@ +require 'locomotive' + +# encoding: utf-8 + namespace :locomotive do desc 'Fetch the Locomotive default site template for the installation' @@ -7,6 +11,145 @@ namespace :locomotive do puts '...done' end + namespace :upgrade do + + desc 'Remove asset collections and convert them into content types' + task :remove_asset_collections => :environment do + puts "asset_collection # ? #{AssetCollection.count}" + + AssetCollection.all.each do |collection| + site = Site.find(collection.attributes['site_id']) + + if collection.internal? + # internal collection => create simple assets without associated to a collection + + collection.assets.each do |tmp_asset| + + sanitized_attributes = tmp_asset.attributes.dup + sanitized_attributes.delete_if { |k, v| [:name, :source_filename].include?(k) } + sanitized_attributes[:_id] = tmp_asset._id + + asset = site.assets.build(sanitized_attributes) + + asset.source = tmp_asset.source.file + + asset.save! + + puts "asset = #{asset.inspect}" + + asset.destroy + end + else + # create content_types reflection of an asset collection + + content_type = site.content_types.build({ + :name => collection.name, + :slug => collection.slug, + :order_by => 'manually' + }) + + # add default custom fields + content_type.content_custom_fields.build(:label => 'Name', :slug => 'name', :kind => 'string', :required => true) + content_type.content_custom_fields.build(:label => 'Source', :slug => 'source', :kind => 'file', :required => true) + + # extra custom fields + collection.asset_custom_fields.each do |field| + content_type.content_custom_fields.build(field.attributes) + end + + # puts "new content_type = #{content_type.inspect}" + + # puts "valid ? #{content_type.valid?.inspect}" + + content_type.save + content_type = ContentType.find(content_type._id) + + # insert data + # puts "collection.assets = #{collection.ordered_assets.inspect} / #{collection.ordered_assets.class}" + + collection.ordered_assets.each do |asset| + attributes = (if asset.custom_fields.blank? + { :created_at => asset.created_at, :updated_at => asset.updated_at } + else + asset.aliased_attributes + end) + + attributes.merge!(:name => asset.name, :_position_in_list => asset.position) + + # puts "attributes = #{attributes.inspect}" + + content = content_type.contents.build(attributes) + + content.source = asset.source.file + + content.save! + + puts "content = #{content.inspect}" + end + + content_type.destroy + end + end + end + + end + end +class TmpAssetUploader < CarrierWave::Uploader::Base + include CarrierWave::RMagick + include Locomotive::CarrierWave::Uploader::Asset + version :thumb, :if => :image? do + process :resize_to_fill => [50, 50] + process :convert => 'png' + end + version :medium, :if => :image? do + process :resize_to_fill => [80, 80] + process :convert => 'png' + end + version :preview, :if => :image? do + process :resize_to_fit => [880, 1100] + process :convert => 'png' + end + def store_dir + self.build_store_dir('sites', model.collection.site_id, 'assets', model.id) + end +end + +# Classes only used during the upgrade mechanism. Will be removed in a few weeks +class AssetCollection + include Locomotive::Mongoid::Document + field :name + field :slug + field :internal, :type => Boolean, :default => false + referenced_in :site + embeds_many :assets, :class_name => 'TmpAsset', :validate => false + custom_fields_for :assets + after_destroy :remove_uploaded_files + scope :internal, :where => { :internal => true } + scope :not_internal, :where => { :internal => false } + def ordered_assets + self.assets.sort { |a, b| (a.position || 0) <=> (b.position || 0) } + end + def self.find_or_create_internal(site) + site.asset_collections.internal.first || site.asset_collections.create(:name => 'system', :slug => 'system', :internal => true) + end +end + +class TmpAsset + include Mongoid::Document + include Mongoid::Timestamps + include CustomFields::ProxyClassEnabler + field :name, :type => String + field :content_type, :type => String + field :width, :type => Integer + field :height, :type => Integer + field :size, :type => Integer + field :position, :type => Integer, :default => 0 + mount_uploader :source, TmpAssetUploader + embedded_in :collection, :class_name => 'AssetCollection', :inverse_of => :assets + def site_id + self.collection.site_id + end +end diff --git a/spec/fixtures/themes/default.zip b/spec/fixtures/themes/default.zip index 404afe89..9d3cc5c9 100644 Binary files a/spec/fixtures/themes/default.zip and b/spec/fixtures/themes/default.zip differ diff --git a/spec/lib/locomotive/import_spec.rb b/spec/lib/locomotive/import_spec.rb index 5935e0dd..516d522b 100644 --- a/spec/lib/locomotive/import_spec.rb +++ b/spec/lib/locomotive/import_spec.rb @@ -20,9 +20,9 @@ describe Locomotive::Import::Job do end it 'adds content types' do - @site.content_types.count.should == 2 + @site.content_types.count.should == 4 content_type = @site.content_types.where(:slug => 'projects').first - content_type.content_custom_fields.size.should == 7 + content_type.content_custom_fields.size.should == 9 end it 'converts correctly the order_by option for content types' do @@ -38,6 +38,8 @@ describe Locomotive::Import::Job do content.name.should == 'Locomotive App' content.thumbnail.url.should_not be_nil content.featured.should == true + content.client.name.should == 'My client #1' + content.team.first.name.should == 'Michael Scott' end it 'inserts theme assets' do