This commit is contained in:
dinedine 2010-10-14 14:37:14 +02:00
commit 282fbb7244
83 changed files with 1569 additions and 371 deletions

4
.gitignore vendored
View File

@ -25,5 +25,9 @@ tmp/*
Capfile Capfile
config/deploy.rb config/deploy.rb
perf/test.rb perf/test.rb
<<<<<<< HEAD
gem_graph.png gem_graph.png
sites/ sites/
=======
sites
>>>>>>> theme

50
Gemfile
View File

@ -1,28 +1,34 @@
# Edit this Gemfile to bundle your application's dependencies. sou rce :rubygems
source :rubygems
# add in all the runtime dependencies # add in all the runtime dependencies
gem "rails", ">= 3.0.0" gem 'rails', '>= 3.0.0'
gem "locomotive_liquid", ">= 2.1.3"
gem "bson_ext", ">= 1.0.8"
gem "mongoid", :git => 'http://github.com/mongoid/mongoid.git'
gem "mongoid_acts_as_tree", "= 0.1.5"
gem "warden"
gem "devise", "= 1.1.2"
gem "haml", "= 3.0.18"
gem "rmagick", "= 2.12.2"
gem "aws"
gem "mimetype-fu"
gem "formtastic", ">= 1.1.0"
gem "carrierwave", "0.5.0.beta2"
gem "actionmailer-with-request"
gem "heroku"
gem "httparty", ">= 0.6.1"
gem "RedCloth"
gem "inherited_resources", ">= 1.1.2"
gem "custom_fields", :git => "http://github.com/locomotivecms/custom_fields.git"
gem 'warden'
gem 'devise', '= 1.1.3'
gem 'mongoid', '2.0.0.beta.19'
gem 'bson_ext', '1.1'
gem 'locomotive_mongoid_acts_as_tree', '0.1.5.1', :require => 'mongoid_acts_as_tree'
gem 'haml', '= 3.0.18'
gem 'locomotive_liquid', '2.2.2', :require => 'liquid'
gem 'formtastic', '>= 1.1.0'
gem 'inherited_resources', '>= 1.1.2'
gem 'rmagick', '= 2.12.2'
gem 'locomotive_carrierwave', :require => 'carrierwave'
gem 'custom_fields', '1.0.0.beta'
gem 'aws'
gem 'mimetype-fu'
gem 'actionmailer-with-request'
gem 'heroku'
gem 'httparty', '>= 0.6.1'
gem 'RedCloth'
gem 'delayed_job', '2.1.0.pre2'
gem 'delayed_job_mongoid', '1.0.0.rc'
gem 'rubyzip'
# The rest of the dependencies are for use when in the locomotive dev environment # The rest of the dependencies are for use when in the locomotive dev environment
@ -46,7 +52,7 @@ group :test do
gem 'capybara' gem 'capybara'
gem 'database_cleaner' gem 'database_cleaner'
gem 'cucumber', "0.8.5" gem 'cucumber', '0.8.5'
gem 'cucumber-rails' gem 'cucumber-rails'
gem 'spork' gem 'spork'
gem 'launchy' gem 'launchy'

View File

@ -15,25 +15,6 @@ GIT
rspec (>= 1.3) rspec (>= 1.3)
yard yard
GIT
remote: http://github.com/locomotivecms/custom_fields.git
revision: eba381b6196c80f2bf8807741297218402df429e
specs:
custom_fields (0.0.0.3)
activesupport (>= 3.0.0)
carrierwave
mongoid (>= 2.0.0.beta.18)
GIT
remote: http://github.com/mongoid/mongoid.git
revision: bc177989f4c4020ec7b16c8ce7cbdea51eba4034
specs:
mongoid (2.0.0.beta.18)
activemodel (~> 3.0)
mongo (= 1.0.9)
tzinfo (~> 0.3.22)
will_paginate (~> 3.0.pre)
GEM GEM
remote: http://rubygems.org/ remote: http://rubygems.org/
specs: specs:
@ -68,14 +49,14 @@ GEM
activesupport (3.0.0) activesupport (3.0.0)
arel (1.0.1) arel (1.0.1)
activesupport (~> 3.0.0) activesupport (~> 3.0.0)
autotest (4.3.2) autotest (4.4.1)
aws (2.3.21) aws (2.3.21)
http_connection http_connection
uuidtools uuidtools
xml-simple xml-simple
bcrypt-ruby (2.1.2) bcrypt-ruby (2.1.2)
bson (1.0.9) bson (1.1)
bson_ext (1.0.9) bson_ext (1.1)
builder (2.1.2) builder (2.1.2)
capybara (0.3.9) capybara (0.3.9)
culerity (>= 0.2.4) culerity (>= 0.2.4)
@ -84,9 +65,11 @@ GEM
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
selenium-webdriver (>= 0.0.3) selenium-webdriver (>= 0.0.3)
carrierwave (0.5.0.beta2) carrierwave (0.5.0)
activesupport (>= 3.0.0.beta4) activesupport (~> 3.0.0)
cgi_multipart_eof_fix (2.5.0) cgi_multipart_eof_fix (2.5.0)
childprocess (0.0.7)
ffi (~> 0.6.3)
columnize (0.3.1) columnize (0.3.1)
configuration (1.1.0) configuration (1.1.0)
crack (0.1.8) crack (0.1.8)
@ -99,8 +82,18 @@ GEM
cucumber-rails (0.3.2) cucumber-rails (0.3.2)
cucumber (>= 0.8.0) cucumber (>= 0.8.0)
culerity (0.2.12) culerity (0.2.12)
custom_fields (1.0.0.beta)
activesupport (>= 3.0.0)
carrierwave
mongoid (>= 2.0.0.beta.18)
daemons (1.1.0) daemons (1.1.0)
database_cleaner (0.5.2) database_cleaner (0.5.2)
delayed_job (2.1.0.pre2)
activesupport (~> 3.0)
daemons
delayed_job_mongoid (1.0.0.rc)
delayed_job (~> 2.1)
mongoid (~> 2.0)
devise (1.1.2) devise (1.1.2)
bcrypt-ruby (~> 2.1.2) bcrypt-ruby (~> 2.1.2)
warden (~> 0.10.7) warden (~> 0.10.7)
@ -124,7 +117,7 @@ GEM
growl-glue (1.0.7) growl-glue (1.0.7)
haml (3.0.18) haml (3.0.18)
has_scope (0.5.0) has_scope (0.5.0)
heroku (1.10.8) heroku (1.10.14)
json_pure (>= 1.2.0, < 1.5.0) json_pure (>= 1.2.0, < 1.5.0)
launchy (~> 0.3.2) launchy (~> 0.3.2)
rest-client (>= 1.4.0, < 1.7.0) rest-client (>= 1.4.0, < 1.7.0)
@ -140,8 +133,13 @@ GEM
configuration (>= 0.0.5) configuration (>= 0.0.5)
rake (>= 0.8.1) rake (>= 0.8.1)
linecache (0.43) linecache (0.43)
locomotive_liquid (2.1.3) locomotive_carrierwave (0.5.0.1)
mail (2.2.6.1) activesupport (~> 3.0)
locomotive_liquid (2.2.2)
locomotive_mongoid_acts_as_tree (0.1.5.1)
bson (>= 0.20.1)
mongoid (<= 2.0.0.beta.19)
mail (2.2.7)
activesupport (>= 2.3.6) activesupport (>= 2.3.6)
mime-types mime-types
treetop (>= 1.4.5) treetop (>= 1.4.5)
@ -149,9 +147,11 @@ GEM
mimetype-fu (0.1.2) mimetype-fu (0.1.2)
mongo (1.0.9) mongo (1.0.9)
bson (>= 1.0.5) bson (>= 1.0.5)
mongoid_acts_as_tree (0.1.5) mongoid (2.0.0.beta.19)
bson (>= 0.20.1) activemodel (~> 3.0)
mongoid (<= 2.0.0) mongo (= 1.0.9)
tzinfo (~> 0.3.22)
will_paginate (~> 3.0.pre)
mongrel (1.1.5) mongrel (1.1.5)
cgi_multipart_eof_fix (>= 2.4) cgi_multipart_eof_fix (>= 2.4)
daemons (>= 1.0.3) daemons (>= 1.0.3)
@ -182,31 +182,32 @@ GEM
rest-client (1.6.1) rest-client (1.6.1)
mime-types (>= 1.16) mime-types (>= 1.16)
rmagick (2.12.2) rmagick (2.12.2)
rspec (2.0.0.beta.22) rspec (2.0.0)
rspec-core (= 2.0.0.beta.22) rspec-core (= 2.0.0)
rspec-expectations (= 2.0.0.beta.22) rspec-expectations (= 2.0.0)
rspec-mocks (= 2.0.0.beta.22) rspec-mocks (= 2.0.0)
rspec-core (2.0.0.beta.22) rspec-core (2.0.0)
rspec-expectations (2.0.0.beta.22) rspec-expectations (2.0.0)
diff-lcs (>= 1.1.2) diff-lcs (>= 1.1.2)
rspec-mocks (2.0.0.beta.22) rspec-mocks (2.0.0)
rspec-core (= 2.0.0.beta.22) rspec-core (= 2.0.0)
rspec-expectations (= 2.0.0.beta.22) rspec-expectations (= 2.0.0)
rspec-rails (2.0.0.beta.22) rspec-rails (2.0.0)
rspec (= 2.0.0.beta.22) rspec (= 2.0.0)
ruby-debug (0.10.3) ruby-debug (0.10.3)
columnize (>= 0.1) columnize (>= 0.1)
ruby-debug-base (~> 0.10.3.0) ruby-debug-base (~> 0.10.3.0)
ruby-debug-base (0.10.3) ruby-debug-base (0.10.3)
linecache (>= 0.3) linecache (>= 0.3)
rubyzip (0.9.4) rubyzip (0.9.4)
selenium-webdriver (0.0.28) selenium-webdriver (0.0.29)
ffi (>= 0.6.1) childprocess (>= 0.0.7)
ffi (~> 0.6.3)
json_pure json_pure
rubyzip rubyzip
spork (0.8.4) spork (0.8.4)
term-ansicolor (1.0.5) term-ansicolor (1.0.5)
thor (0.14.2) thor (0.14.3)
treetop (1.4.8) treetop (1.4.8)
polyglot (>= 0.3.1) polyglot (>= 0.3.1)
trollop (1.16.2) trollop (1.16.2)
@ -226,14 +227,15 @@ DEPENDENCIES
actionmailer-with-request actionmailer-with-request
autotest autotest
aws aws
bson_ext (>= 1.0.8) bson_ext (= 1.1)
capybara capybara
carrierwave (= 0.5.0.beta2)
cgi_multipart_eof_fix cgi_multipart_eof_fix
cucumber (= 0.8.5) cucumber (= 0.8.5)
cucumber-rails cucumber-rails
custom_fields! custom_fields (= 1.0.0.beta)
database_cleaner database_cleaner
delayed_job (= 2.1.0.pre2)
delayed_job_mongoid (= 1.0.0.rc)
devise (= 1.1.2) devise (= 1.1.2)
factory_girl_rails factory_girl_rails
fastthread fastthread
@ -244,16 +246,18 @@ DEPENDENCIES
httparty (>= 0.6.1) httparty (>= 0.6.1)
inherited_resources (>= 1.1.2) inherited_resources (>= 1.1.2)
launchy launchy
locomotive_liquid (>= 2.1.3) locomotive_carrierwave
locomotive_liquid (= 2.2.2)
locomotive_mongoid_acts_as_tree (= 0.1.5.1)
mimetype-fu mimetype-fu
mocha! mocha!
mongoid! mongoid (= 2.0.0.beta.19)
mongoid_acts_as_tree (= 0.1.5)
mongrel mongrel
pickle! pickle!
rails (>= 3.0.0) rails (>= 3.0.0)
rmagick (= 2.12.2) rmagick (= 2.12.2)
rspec-rails (>= 2.0.0.beta.18) rspec-rails (>= 2.0.0.beta.18)
ruby-debug ruby-debug
rubyzip
spork spork
warden warden

View File

@ -8,7 +8,7 @@ If we have to give only 5 main features to describe our application, there will
* nice looking UI (see http://www.locomotiveapp.org for some screenshots) * nice looking UI (see http://www.locomotiveapp.org for some screenshots)
* flexible content types * flexible content types
* playing smoothly with Heroku and MongoHQ * playing smoothly with Heroku and MongoHQ
* inline editing (coming soon) * inline editing (beta)
h2. Strategy / Development status h2. Strategy / Development status
@ -19,12 +19,13 @@ h2. Gems
Here is a short list of main gems used in the application. Here is a short list of main gems used in the application.
* Rails 3 RC * Rails 3.0
* Mongoid 2.0.0.beta 16 (with MongoDB 1.6) * Mongoid 2.0.0.beta 17 (with MongoDB 1.6)
* Liquid * Liquid
* Devise * Devise
* Carrierwave * Carrierwave
* Haml * Haml
* Delayed job
h2. Installation h2. Installation

View File

@ -22,8 +22,8 @@ module Admin
end end
def set_collections_and_current_collection def set_collections_and_current_collection
@asset_collections = current_site.asset_collections @asset_collections = current_site.asset_collections.not_internal.order_by([[:name, :asc]])
@asset_collection = @asset_collections.find(params[:collection_id]) @asset_collection = current_site.asset_collections.find(params[:collection_id])
end end
end end

View File

@ -3,5 +3,9 @@ module Admin
sections 'contents' sections 'contents'
def destroy
destroy! { admin_pages_url }
end
end end
end end

View File

@ -0,0 +1,51 @@
module Admin
class ImportsController < BaseController
sections 'settings', 'site'
actions :show, :new, :create
def show
@job = Delayed::Job.where({ :job_type => 'import', :site_id => current_site.id }).last
respond_to do |format|
format.html do
redirect_to new_admin_import_url if @job.nil?
end
format.json { render :json => {
:step => @job.nil? ? 'done' : @job.step,
:failed => @job && @job.last_error.present?
} }
end
end
def new; end
def create
if params[:zipfile].blank?
@error = t('errors.messages.blank')
flash[:alert] = t('flash.admin.imports.create.alert')
render 'new'
else
path = self.store_zipfile!
job = Locomotive::Import::Job.new(path, current_site)
Delayed::Job.enqueue job, { :site => current_site, :job_type => 'import' }
flash[:notice] = t('flash.admin.imports.create.notice')
redirect_to admin_import_url
end
end
protected
def store_zipfile!
file = CarrierWave::SanitizedFile.new(params[:zipfile])
file.move_to(File.join(Rails.root, 'tmp', 'files', current_site.id.to_s))
file.path
end
end
end

View File

@ -1,12 +1,14 @@
module Admin module Admin
class SnippetsController < BaseController class SnippetsController < BaseController
sections 'settings' sections 'settings', 'theme_assets'
respond_to :json, :only => :update respond_to :json, :only => :update
def index def destroy
@snippets = current_site.snippets.order_by([[:name, :asc]]) destroy! do |format|
format.html { redirect_to admin_theme_assets_url }
end
end end
end end

View File

@ -4,22 +4,30 @@ module Admin
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
extend ActionView::Helpers::SanitizeHelper::ClassMethods extend ActionView::Helpers::SanitizeHelper::ClassMethods
include ActionView::Helpers::TextHelper include ActionView::Helpers::TextHelper
include ActionView::Helpers::NumberHelper
sections 'settings', 'theme_assets' sections 'settings', 'theme_assets'
respond_to :json, :only => [:create, :update] respond_to :json, :only => [:create, :update]
def index def index
assets = current_site.theme_assets.all @assets = current_site.theme_assets.visible(params[:all]).order_by([[:slug, :asc]])
@non_image_assets = assets.find_all { |a| a.stylesheet? || a.javascript? } @assets = @assets.group_by { |a| a.folder.split('/').first.to_sym }
@image_assets = assets.find_all { |a| a.image? } @js_and_css_assets = (@assets[:javascripts] || []) + (@assets[:stylesheets] || [])
@flash_assets = assets.find_all { |a| a.movie? }
if request.xhr? if request.xhr?
@images = @assets[:images]
render :action => 'images', :layout => false and return render :action => 'images', :layout => false and return
else
@snippets = current_site.snippets.order_by([[:name, :asc]]).all.to_a
end end
end end
def edit
resource.performing_plain_text = true if resource.stylesheet_or_javascript?
edit!
end
def create def create
params[:theme_asset] = { :source => params[:file] } if params[:file] params[:theme_asset] = { :source => params[:file] } if params[:file]
@ -27,11 +35,10 @@ module Admin
success.json do success.json do
render :json => { render :json => {
:status => 'success', :status => 'success',
:name => truncate(@theme_asset.slug, :length => 22),
:slug => @theme_asset.slug,
:url => @theme_asset.source.url, :url => @theme_asset.source.url,
:vignette_url => @theme_asset.vignette_url, :local_path => @theme_asset.local_path(true),
:shortcut_url => @theme_asset.shortcut_url :size => number_to_human_size(@theme_asset.size),
:date => l(@theme_asset.updated_at, :format => :short)
} }
end end
failure.json { render :json => { :status => 'error' } } failure.json { render :json => { :status => 'error' } }

View File

@ -1,8 +1,8 @@
module Admin::CustomFieldsHelper module Admin::CustomFieldsHelper
def options_for_field_kind(selected = nil) def options_for_field_kind
options = %w{String Text Category Boolean Date File}.map do |kind| options = %w{string text category boolean date file}.map do |kind|
[t("admin.custom_fields.kind.#{kind.downcase}"), kind] [t("admin.custom_fields.kind.#{kind}"), kind]
end end
end end

View File

@ -32,8 +32,12 @@ class Asset
end end
end end
def site_id # needed by the uploader of custom fields
self.collection.site_id
end
def to_liquid def to_liquid
{ :url => self.source.url }.merge(self.attributes) Locomotive::Liquid::Drops::Asset.new(self)
end end
end end

View File

@ -29,6 +29,10 @@ class ContentInstance
alias :visible? :_visible? alias :visible? :_visible?
def site_id # needed by the uploader of custom fields
self.content_type.site_id
end
def visible? def visible?
self._visible || self._visible.nil? self._visible || self._visible.nil?
end end

View File

@ -26,16 +26,21 @@ class Snippet
protected protected
def normalize_slug def normalize_slug
# TODO: refactor it
self.slug = self.name.clone if self.slug.blank? && self.name.present? self.slug = self.name.clone if self.slug.blank? && self.name.present?
self.slug.slugify!(:without_extension => true, :downcase => true) if self.slug.present? self.slug.slugify!(:without_extension => true, :downcase => true) if self.slug.present?
end end
def update_templates def update_templates
return unless (self.site rescue false) # not run if the site is being destroyed
pages = self.site.pages.any_in(:snippet_dependencies => [self.slug]).to_a pages = self.site.pages.any_in(:snippet_dependencies => [self.slug]).to_a
pages.each do |page| pages.each do |page|
self._change_snippet_inside_template(page.template.root) self._change_snippet_inside_template(page.template.root)
page.instance_variable_set(:@template_changed, true)
page.send(:_serialize_template) && page.save page.send(:_serialize_template) && page.save
end end
end end
@ -44,11 +49,11 @@ class Snippet
case node case node
when Locomotive::Liquid::Tags::Snippet when Locomotive::Liquid::Tags::Snippet
node.refresh(self) if node.slug == self.slug node.refresh(self) if node.slug == self.slug
when Locomotive::Liquid::Tags::Block when Locomotive::Liquid::Tags::InheritedBlock
self._change_snippet_inside_template(node.parent) if node.parent self._change_snippet_inside_template(node.parent) if node.parent
else else
if node.respond_to?(:nodelist) if node.respond_to?(:nodelist)
node.nodelist.each do |child| (node.nodelist || []).each do |child|
self._change_snippet_inside_template(child) self._change_snippet_inside_template(child)
end end
end end

View File

@ -2,16 +2,14 @@ class ThemeAsset
include Locomotive::Mongoid::Document include Locomotive::Mongoid::Document
## Extensions ##
include Models::Extensions::Asset::Vignette
## fields ## ## fields ##
field :slug field :local_path
field :content_type field :content_type
field :width, :type => Integer field :width, :type => Integer
field :height, :type => Integer field :height, :type => Integer
field :size, :type => Integer field :size, :type => Integer
field :plain_text field :folder, :default => nil
field :hidden, :type => Boolean, :default => false
mount_uploader :source, ThemeAssetUploader mount_uploader :source, ThemeAssetUploader
## associations ## ## associations ##
@ -19,22 +17,25 @@ class ThemeAsset
## indexes ## ## indexes ##
index :site_id index :site_id
index [[:content_type, Mongo::ASCENDING], [:slug, Mongo::ASCENDING], [:site_id, Mongo::ASCENDING]] index [[:site_id, Mongo::ASCENDING], [:local_path, Mongo::ASCENDING]]
## callbacks ## ## callbacks ##
before_validation :sanitize_slug
before_validation :store_plain_text before_validation :store_plain_text
before_save :set_slug before_save :sanitize_folder
before_save :build_local_path
## validations ## ## validations ##
validate :extname_can_not_be_changed
validates_presence_of :site, :source validates_presence_of :site, :source
validates_presence_of :slug, :if => Proc.new { |a| a.new_record? && a.performing_plain_text? } validates_presence_of :plain_text_name, :if => Proc.new { |a| a.performing_plain_text? }
validates_uniqueness_of :slug, :scope => [:site_id, :content_type] validates_uniqueness_of :local_path, :scope => :site_id
validates_integrity_of :source validates_integrity_of :source
validate :content_type_can_not_changed
## named scopes ##
scope :visible, lambda { |all| all ? {} : { :where => { :hidden => false } } }
## accessors ## ## accessors ##
attr_accessor :performing_plain_text attr_accessor :plain_text_name, :plain_text, :performing_plain_text
## methods ## ## methods ##
@ -48,77 +49,90 @@ class ThemeAsset
self.stylesheet? || self.javascript? self.stylesheet? || self.javascript?
end end
def plain_text def local_path(short = false)
if self.stylesheet_or_javascript? if short
self.plain_text = self.source.read if read_attribute(:plain_text).blank? self.read_attribute(:local_path).gsub(/^#{self.content_type.pluralize}\//, '')
read_attribute(:plain_text)
else else
nil self.read_attribute(:local_path)
end end
end end
def plain_text=(source) def plain_text_name
self.performing_plain_text = true if self.performing_plain_text.nil? if not @plain_text_name_changed
write_attribute(:plain_text, source) @plain_text_name ||= self.safe_source_filename
end
@plain_text_name.gsub(/(\.[a-z0-9A-Z]+)$/, '') rescue nil
end
def plain_text_name=(name)
@plain_text_name_changed = true
@plain_text_name = name
end
def plain_text
@plain_text ||= self.source.read
end end
def performing_plain_text? def performing_plain_text?
return true if !self.new_record? && self.stylesheet_or_javascript? && self.errors.empty? Boolean.set(self.performing_plain_text) || false
!(self.performing_plain_text.blank? || self.performing_plain_text == 'false' || self.performing_plain_text == false)
end end
def store_plain_text def store_plain_text
return if self.plain_text.blank? data = self.performing_plain_text? ? self.plain_text : self.source.read
# replace /theme/<content_type>/<slug> occurences by the real amazon S3 url or local files return if !self.stylesheet_or_javascript? || self.plain_text_name.blank? || data.blank?
sanitized_source = self.plain_text.gsub(/(\/theme\/([a-z]+)\/([a-z_\-0-9]+)\.[a-z]{2,3})/) do |url|
content_type, slug = url.split('/')[2..-1]
content_type = content_type.singularize sanitized_source = self.escape_shortcut_urls(data)
slug = slug.split('.').first
if asset = self.site.theme_assets.where(:content_type => content_type, :slug => slug).first
asset.source.url
else
url
end
end
self.source = CarrierWave::SanitizedFile.new({ self.source = CarrierWave::SanitizedFile.new({
:tempfile => StringIO.new(sanitized_source), :tempfile => StringIO.new(sanitized_source),
:filename => "#{self.slug}.#{self.stylesheet? ? 'css' : 'js'}" :filename => "#{self.plain_text_name}.#{self.stylesheet? ? 'css' : 'js'}"
}) })
end end
def shortcut_url # ex: /theme/stylesheets/application.css is a shortcut for a theme asset (content_type => stylesheet, slug => 'application')
File.join('/theme', self.content_type.pluralize, "#{self.slug}#{File.extname(self.source_filename)}")
rescue
''
end
def to_liquid def to_liquid
{ :url => self.source.url }.merge(self.attributes) { :url => self.source.url }.merge(self.attributes)
end end
protected protected
def sanitize_slug def safe_source_filename
self.slug.slugify!(:underscore => true) if self.slug.present? self.source_filename || self.source.send(:original_filename) rescue nil
end end
def set_slug def sanitize_folder
if self.slug.blank? self.folder = self.content_type.pluralize if self.folder.blank?
self.slug = File.basename(self.source_filename, File.extname(self.source_filename))
self.sanitize_slug # no accents, no spaces, no leading and ending trails
self.folder = ActiveSupport::Inflector.transliterate(self.folder).gsub(/(\s)+/, '_').gsub(/^\//, '').gsub(/\/$/, '').downcase
# folder should begin by a root folder
if (self.folder =~ /^(stylesheets|javascripts|images|media|fonts)/).nil?
self.folder = File.join(self.content_type.pluralize, self.folder)
end end
end end
def extname_can_not_be_changed def build_local_path
return if self.new_record? self.local_path = File.join(self.folder, self.safe_source_filename)
end
if File.extname(self.source.file.original_filename) != File.extname(self.source_filename) def escape_shortcut_urls(text)
self.errors.add(:source, :extname_changed) return if text.blank?
text.gsub(/[("'](\/(stylesheets|javascripts|images|media)\/((.+)\/)*([a-z_\-0-9]+)\.[a-z]{2,3})[)"']/) do |path|
sanitized_path = path.gsub(/[("')]/, '').gsub(/^\//, '')
if asset = self.site.theme_assets.where(:local_path => sanitized_path).first
"#{path.first}#{asset.source.url}#{path.last}"
else
path
end
end end
end end
def content_type_can_not_changed
self.errors.add(:source, :extname_changed) if !self.new_record? && self.content_type_changed?
end
end end

View File

@ -31,7 +31,7 @@ class AssetUploader < CarrierWave::Uploader::Base
process :set_size process :set_size
process :set_width_and_height process :set_width_and_height
def set_content_type def set_content_type(*args)
value = :other value = :other
content_type = file.content_type == 'application/octet-stream' ? File.mime_type?(original_filename) : file.content_type content_type = file.content_type == 'application/octet-stream' ? File.mime_type?(original_filename) : file.content_type
@ -48,7 +48,7 @@ class AssetUploader < CarrierWave::Uploader::Base
model.content_type = value model.content_type = value
end end
def set_size def set_size(*args)
model.size = file.size model.size = file.size
end end
@ -61,12 +61,12 @@ class AssetUploader < CarrierWave::Uploader::Base
def self.content_types def self.content_types
{ {
:image => ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png', 'image/jpg'], :image => ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png', 'image/jpg'],
:movie => [/^video/, 'application/x-shockwave-flash', 'application/x-swf'], :video => [/^video/, 'application/x-shockwave-flash', 'application/x-swf'],
:audio => [/^audio/, 'application/ogg', 'application/x-mp3'], :audio => [/^audio/, 'application/ogg', 'application/x-mp3'],
:pdf => ['application/pdf', 'application/x-pdf'], :pdf => ['application/pdf', 'application/x-pdf'],
:stylesheet => ['text/css'], :stylesheet => ['text/css'],
:javascript => ['text/javascript', 'text/js', 'application/x-javascript', 'application/javascript'], :javascript => ['text/javascript', 'text/js', 'application/x-javascript', 'application/javascript'],
:font => ['application/x-font-ttf', 'application/vnd.ms-fontobject'] :font => ['application/x-font-ttf', 'application/vnd.ms-fontobject', 'image/svg+xml', 'application/x-woff']
} }
end end

View File

@ -6,27 +6,16 @@ class ThemeAssetUploader < AssetUploader
process :set_size process :set_size
process :set_width_and_height process :set_width_and_height
version :thumb do
process :resize_to_fill => [50, 50]
process :convert => 'png'
end
version :medium do
process :resize_to_fill => [80, 80]
process :convert => 'png'
end
version :preview do
process :resize_to_fit => [880, 1100]
process :convert => 'png'
end
def store_dir def store_dir
"sites/#{model.site_id}/themes/#{model.id}" File.join('sites', model.site_id.to_s, 'theme', model.folder_was || model.folder)
end
def stale_model?
!model.new_record? && model.folder_changed?
end end
def extension_white_list def extension_white_list
%w(jpg jpeg gif png css js swf flv) %w(jpg jpeg gif png css js swf flv eot svg ttf woff)
end end
end end

View File

@ -4,6 +4,7 @@
= render 'admin/shared/menu/settings' = render 'admin/shared/menu/settings'
- content_for :buttons do - content_for :buttons do
= admin_button_tag :import, new_admin_import_url, :class => 'new'
= admin_button_tag t('.new_membership'), new_admin_membership_url, :class => 'new' = admin_button_tag t('.new_membership'), new_admin_membership_url, :class => 'new'
%p!= t('.help') %p!= t('.help')

View File

@ -0,0 +1,21 @@
- title t('.title')
- content_for :submenu do
= render 'admin/shared/menu/settings'
%p!= t('.help')
= form_tag admin_import_url, :multipart => true, :class => 'formtastic import' do
%fieldset.inputs
%legend
%span= t('formtastic.titles.upload')
%ol
%li{ :class => "file #{'error' if @error} required" }
= label_tag 'zipfile', t('formtastic.labels.import.new.source')
= file_field_tag 'zipfile'
- if @error
%p.inline-errors= @error
%p.inline-hints= t('formtastic.hints.import.source')
= render 'admin/shared/form_actions', :button_label => :send

View File

@ -0,0 +1,19 @@
- content_for :head do
= javascript_include_tag 'admin/plugins/json2', 'admin/plugins/smartupdater', 'admin/import'
- title t('.title')
- content_for :submenu do
= render 'admin/shared/menu/settings'
%p!= t('.help')
%ul{ :id => 'import-steps', :class => 'list', :'data-url' => admin_import_url(:json), :'data-success-message' => t('.message.success'), :'data-failure-message' => t('.message.failure') }
- %w(site content_types assets asset_collections snippets pages).each do |step|
%li{ :id => "#{step}-step" }
%em
%strong
= link_to t(".steps.#{step}"), '#'
.more
.states
&nbsp;

View File

@ -1,7 +1,7 @@
%h1 %h1
- if current_admin.sites.size > 1 - if current_admin.sites.size > 1
= form_tag new_admin_cross_domain_session_url, :method => 'get' do = form_tag new_admin_cross_domain_session_url, :method => 'get' do
= select_tag 'target_id', options_for_select(current_admin.sites.collect { |site| [site.name, site.id] }, current_site.id), :id => 'site-selector' = select_tag 'target_id', options_for_select(current_admin.sites.collect { |site| [truncate(site.name, :length => 32), site.id] }, current_site.id), :id => 'site-selector'
= submit_tag 'Switch', :style => 'display: none' = submit_tag 'Switch', :style => 'display: none'
- else - else
= link_to current_site.name, admin_root_url, :class => 'single' = link_to current_site.name, admin_root_url, :class => 'single'

View File

@ -1,5 +1,5 @@
%ul %ul
= admin_submenu_item 'site', edit_admin_current_site_url = admin_submenu_item 'site', edit_admin_current_site_url
= admin_submenu_item 'snippets', admin_snippets_url / = admin_submenu_item 'snippets', admin_snippets_url
= admin_submenu_item 'theme_assets', admin_theme_assets_url = admin_submenu_item 'theme_assets', admin_theme_assets_url
= admin_submenu_item 'account', edit_admin_my_account_url = admin_submenu_item 'account', edit_admin_my_account_url

View File

@ -1,4 +1,5 @@
%li %li
%em
%strong= link_to snippet.name, edit_admin_snippet_path(snippet) %strong= link_to snippet.name, edit_admin_snippet_path(snippet)
.more .more
%span!= t('.updated_at') %span!= t('.updated_at')

View File

@ -12,4 +12,4 @@
= render 'form', :f => form = render 'form', :f => form
= render 'admin/shared/form_actions', :back_url => admin_snippets_url, :button_label => :update = render 'admin/shared/form_actions', :back_url => admin_theme_assets_url, :button_label => :update

View File

@ -9,4 +9,4 @@
= render 'form', :f => form = render 'form', :f => form
= render 'admin/shared/form_actions', :back_url => admin_snippets_url, :button_label => :create = render 'admin/shared/form_actions', :back_url => admin_theme_assets_url, :button_label => :create

View File

@ -1,12 +1,13 @@
- per_row = local_assigns[:per_row] || 6
- asset_counter = local_assigns[:asset_counter] || 0
- edit = local_assigns.key?(:edit) ? edit : true - edit = local_assigns.key?(:edit) ? edit : true
%li{ :class => "#{asset.new_record? ? 'new-asset' : 'asset'} #{'last' if (asset_counter + 1) % per_row == 0}"} %li{ :class => "#{asset.new_record? ? 'new-asset' : 'asset'} #{'hidden' if asset.hidden?}" }
%h4= link_to truncate(asset.slug, :length => 18), edit ? edit_admin_theme_asset_path(asset) : asset.source.url, :"data-slug" => asset.slug, :"data-shortcut-url" => asset.shortcut_url %em
.image %strong= link_to asset.local_path(!edit), edit ? edit_admin_theme_asset_path(asset) : asset.source.url, :'data-local-path' => asset.local_path
.inside .more
= vignette_tag(asset) %span.size= number_to_human_size(asset.size)
- if edit &mdash;
.actions %span!= t('.updated_at')
= link_to image_tag('admin/list/icons/cross.png'), admin_theme_asset_path(asset), :class => 'remove', :confirm => t('admin.messages.confirm'), :method => :delete %span.date= l asset.updated_at, :format => :short
- if edit
= link_to image_tag('admin/list/icons/trash.png'), admin_theme_asset_path(asset), :class => 'remove', :confirm => t('admin.messages.confirm'), :method => :delete

View File

@ -4,24 +4,23 @@
= f.hidden_field :performing_plain_text = f.hidden_field :performing_plain_text
#file-selector{ :class => "selector #{'hidden' if @theme_asset.performing_plain_text?}" } #file-selector{ :class => "selector #{'hidden' if @theme_asset.stylesheet_or_javascript?}" }
= f.inputs :name => :information do = f.inputs :name => :information do
= f.input :source = f.input :source
= f.input :slug
- if @theme_asset.new_record? || @theme_asset.stylesheet_or_javascript? - if @theme_asset.new_record? || @theme_asset.stylesheet_or_javascript?
%span.alt %span.alt
!= t('admin.theme_assets.form.choose_plain_text') != t('admin.theme_assets.form.choose_plain_text')
- if allow_plain_text_editing?(@theme_asset) - if allow_plain_text_editing?(@theme_asset)
#text-selector{ :class => "selector #{'hidden' if !@theme_asset.performing_plain_text?}", :style => "#{'display: none' if !@theme_asset.performing_plain_text?}" } #text-selector{ :class => "selector #{'hidden' if !@theme_asset.stylesheet_or_javascript?}", :style => "#{'display: none' if !@theme_asset.stylesheet_or_javascript?}" }
= f.inputs :name => :code, :class => 'inputs code' do = f.inputs :name => :code, :class => 'inputs code' do
- if @theme_asset.new_record? - if @theme_asset.new_record?
= f.input :slug = f.input :plain_text_name
= f.custom_input :content_type do = f.custom_input :content_type do
= f.select :content_type, ["stylesheet", "javascript"] = f.select :content_type, %w(stylesheet javascript)
= f.custom_input :plain_text, :css => 'full', :with_label => false do = f.custom_input :plain_text, :css => 'full', :with_label => false do
%code{ :class => (@theme_asset.size && @theme_asset.size > 40000 ? 'nude' : (@theme_asset.content_type || 'stylesheet')) } %code{ :class => (@theme_asset.size && @theme_asset.size > 40000 ? 'nude' : (@theme_asset.content_type || 'stylesheet')) }
@ -32,9 +31,7 @@
%span.alt %span.alt
!= t('admin.theme_assets.form.choose_file') != t('admin.theme_assets.form.choose_file')
- if @theme_asset.image? = f.foldable_inputs :name => :options do
= f.foldable_inputs :name => "#{t('formtastic.titles.preview')} #{image_dimensions_and_size(@theme_asset)}", :class => 'preview' do = f.input :folder
%li = f.custom_input :hidden, :css => 'toggle' do
.image = f.check_box :hidden
.inside
= image_tag(@theme_asset.source.url(:preview))

View File

@ -6,7 +6,7 @@
- content_for :buttons do - content_for :buttons do
= admin_button_tag t('admin.theme_assets.index.new'), new_admin_theme_asset_url, :class => 'new' = admin_button_tag t('admin.theme_assets.index.new'), new_admin_theme_asset_url, :class => 'new'
%p!= t('.help', :url => @theme_asset.source.url, :shortcut_url => @theme_asset.shortcut_url) %p!= t('.help', :url => @theme_asset.source.url)
= semantic_form_for @theme_asset, :url => admin_theme_asset_url(@theme_asset), :html => { :multipart => true, :class => 'save-with-shortcut' } do |form| = semantic_form_for @theme_asset, :url => admin_theme_asset_url(@theme_asset), :html => { :multipart => true, :class => 'save-with-shortcut' } do |form|

View File

@ -4,12 +4,12 @@
.actions .actions
= admin_button_tag t('admin.theme_assets.index.new'), admin_theme_assets_url(:json), :class => 'button small add', :id => 'upload-link' = admin_button_tag t('admin.theme_assets.index.new'), admin_theme_assets_url(:json), :class => 'button small add', :id => 'upload-link'
- if @image_assets.empty? - if @images.empty?
%p.no-items!= t('.no_items') %p.no-items!= t('.no_items')
%ul.assets %ul.list.theme-assets
= render 'asset', :asset => current_site.theme_assets.build, :edit => false = render 'asset', :asset => current_site.theme_assets.build(:updated_at => Time.now, :local_path => 'images/new.jpg', :content_type => 'image'), :edit => false
= render :partial => 'asset', :collection => @image_assets, :locals => { :per_row => 3, :edit => false } = render :partial => 'asset', :collection => @images, :locals => { :edit => false }
%li.clear %li.clear

View File

@ -4,33 +4,48 @@
= render 'admin/shared/menu/settings' = render 'admin/shared/menu/settings'
- content_for :buttons do - content_for :buttons do
= admin_button_tag t('admin.theme_assets.index.all'), all_admin_theme_assets_url, :class => 'show'
= admin_button_tag t('admin.snippets.index.new'), new_admin_snippet_url, :class => 'new'
= admin_button_tag :new, new_admin_theme_asset_url, :class => 'new' = admin_button_tag :new, new_admin_theme_asset_url, :class => 'new'
%p!= t('.help') %p!= t('.help')
%h3!= t('.snippets')
- if @snippets.empty?
%p.no-items!= t('.no_items', :url => new_admin_snippet_url)
- else
%ul.list.theme-assets
= render @snippets
%br
%h3!= t('.css_and_js') %h3!= t('.css_and_js')
- if @non_image_assets.empty? - if @js_and_css_assets.empty?
%p.no-items!= t('.no_items', :url => new_admin_theme_asset_url) %p.no-items!= t('.no_items', :url => new_admin_theme_asset_url)
- else - else
%ul.assets %ul.list.theme-assets
= render :partial => 'asset', :collection => @non_image_assets = render :partial => 'asset', :collection => @js_and_css_assets
%li.clear
%br %br
%h3!= t('.images') %h3!= t('.images')
- if @image_assets.empty? - if @assets[:images].nil?
%p.no-items!= t('.no_items', :url => new_admin_theme_asset_url) %p.no-items!= t('.no_items', :url => new_admin_theme_asset_url)
- else - else
%ul.assets %ul.list.theme-assets
= render :partial => 'asset', :collection => @image_assets = render :partial => 'asset', :collection => @assets[:images]
%li.clear
- if @assets[:fonts]
- if not @flash_assets.empty?
%br %br
%h3!= t('.flashes') %h3!= t('.fonts')
%ul.assets %ul.list.theme-assets
= render :partial => 'asset', :collection => @flash_assets = render :partial => 'asset', :collection => @assets[:fonts]
%li.clear
- if @assets[:media]
%br
%h3!= t('.media')
%ul.list.theme-assets
= render :partial => 'asset', :collection => @assets[:media]

View File

@ -46,5 +46,7 @@ module Locomotive
# Configure sensitive parameters which will be filtered from the log file. # Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters << :password config.filter_parameters << :password
config.middleware.insert_after ::ActionDispatch::Static, '::Locomotive::Middlewares::Fonts', :path => %r{^/fonts}
end end
end end

View File

@ -0,0 +1 @@
Haml::Template.options[:ugly] = true # improve performance in dev

View File

@ -8,3 +8,8 @@
# inflect.irregular 'person', 'people' # inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep ) # inflect.uncountable %w( fish sheep )
# end # end
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'media', 'media'
end

View File

@ -1,5 +1,4 @@
require File.dirname(__FILE__) + '/../../lib/locomotive.rb' require File.dirname(__FILE__) + '/../../lib/locomotive.rb'
require File.dirname(__FILE__) + '/../../lib/core_ext.rb'
Locomotive.configure do |config| Locomotive.configure do |config|
# if not defined, locomotive will use example.com as main domain name. Remove prefix www from your domain name. # if not defined, locomotive will use example.com as main domain name. Remove prefix www from your domain name.

View File

@ -30,6 +30,7 @@ en:
back: Back without saving back: Back without saving
create: Create create: Create
update: Update update: Update
send: Send
errors: errors:
"500": "500":
@ -131,6 +132,7 @@ en:
current_sites: current_sites:
edit: edit:
import: import
new_membership: add account new_membership: add account
help: "The site name can be updated by clicking it." help: "The site name can be updated by clicking it."
ask_for_name: "Please type the new site name" ask_for_name: "Please type the new site name"
@ -156,19 +158,23 @@ en:
theme_assets: theme_assets:
index: index:
title: Listing theme files title: Listing theme files
help: "Theme assets represent files needed by layouts and snippets. If you need to manage an image gallery, go to the Assets section instead." help: "The theme files section is the place where you manage the files needed by your layout, ...etc. If you need to manage an image gallery, go to the Assets section instead."
all: all assets
new: new file new: new file
snippets: Snippets
css_and_js: Style and javascript css_and_js: Style and javascript
fonts: Fonts fonts: Fonts
images: Images images: Images
flashes: Flash media: Media
no_items: "There are no files for now. Just click <a href=\"%{url}\">here</a> to create the first one." no_items: "There are no files for now. Just click <a href=\"%{url}\">here</a> to create the first one."
asset:
updated_at: Updated at
new: new:
title: New file title: New file
help: "You have the choice to either upload any file or to copy/paste a stylesheet or a javascript in plain text." help: "You have the choice to either upload any file or to copy/paste a stylesheet or a javascript in plain text."
edit: edit:
title: "Editing %{file}" title: "Editing %{file}"
help: "You can insert the following shortcut url in your stylesheets: <strong>%{shortcut_url}</strong> OR use the direct url <strong>%{url}</strong>" help: "This asset is accessible from the following url: <strong>%{url}</strong>"
form: form:
picker_link: Insert a file into the code picker_link: Insert a file into the code
choose_file: Choose file choose_file: Choose file
@ -242,6 +248,24 @@ en:
title: Cross-domain authentication title: Cross-domain authentication
notice: You will be redirected to the website in a couple of seconds. notice: You will be redirected to the website in a couple of seconds.
imports:
new:
title: Import
help: "Be careful when you upload a new theme for your existing website, your current data could be modified or even removed."
show:
title: Import in progress
help: "Your site is being updated from the theme zip file you have just uploaded. It lasts a couple of seconds."
steps:
site: Site information
content_types: Custom content types
assets: Theme files
asset_collections: Asset collections
snippets: Snippets
pages: Pages
messages:
success: "Your site was successfully updated."
failure: "The import did not work."
formtastic: formtastic:
titles: titles:
information: General information information: General information
@ -261,6 +285,7 @@ en:
other_fields: Other information other_fields: Other information
presentation: Presentation presentation: Presentation
attributes: Attributes attributes: Attributes
upload: Upload
labels: labels:
page: page:
raw_template: Template raw_template: Template
@ -272,6 +297,9 @@ en:
custom_fields: custom_fields:
field: field:
_alias: Alias _alias: Alias
import:
new:
source: File
hints: hints:
page: page:
@ -292,4 +320,6 @@ en:
field: field:
_alias: "Property available in liquid templates" _alias: "Property available in liquid templates"
hint: "Text displayed in the model form just below the field" hint: "Text displayed in the model form just below the field"
import:
source: "A zipfile containing a database.yml along with assets and templates"

View File

@ -40,6 +40,7 @@ fr:
back: Retour sans sauvegarder back: Retour sans sauvegarder
create: Créer create: Créer
update: Mettre à jour update: Mettre à jour
send: Envoyer
custom_fields: custom_fields:
edit: edit:
@ -131,6 +132,7 @@ fr:
current_sites: current_sites:
edit: edit:
import: importer
new_membership: ajouter compte new_membership: ajouter compte
help: "Le nom du site est modifiable en cliquant dessus." help: "Le nom du site est modifiable en cliquant dessus."
ask_for_name: "Veuillez entrer le nouveau nom" ask_for_name: "Veuillez entrer le nouveau nom"
@ -156,13 +158,17 @@ fr:
theme_assets: theme_assets:
index: index:
title: Liste des fichiers du thème title: Liste des fichiers du thème
help: "Les fichiers du thème sont utilisés par les gabarits et les snippets. Si vous avez besoin d'une galerie d'images, la section Média est plus adéquate." help: "Les fichiers du thème sont utilisés pour construire le gabarit de vos pages. Si vous avez besoin d'une galerie d'images, la section Média est plus adéquate."
all: tous les fichiers
new: nouveau fichier new: nouveau fichier
snippets: Snippets
css_and_js: Style et javascript css_and_js: Style et javascript
images: Images images: Images
flashes: Flash media: Media
fonts: Polices fonts: Polices
no_items: "Il n'existe pas de fichiers. Vous pouvez commencer par créer un <a href='%{url}'>ici</a>." no_items: "Il n'existe pas de fichiers. Vous pouvez commencer par créer un <a href='%{url}'>ici</a>."
asset:
updated_at: Mis à jour le
new: new:
title: Nouveau fichier title: Nouveau fichier
help: "Vous avez le choix de soit uploader n'importe quel fichier ou bien soit de copier/coller du code css ou javascript." help: "Vous avez le choix de soit uploader n'importe quel fichier ou bien soit de copier/coller du code css ou javascript."
@ -241,6 +247,24 @@ fr:
title: Transfert vers un autre site title: Transfert vers un autre site
notice: Vous allez être redirigé(e) vers le site dans quelques secondes. notice: Vous allez être redirigé(e) vers le site dans quelques secondes.
imports:
new:
title: Import
help: "Faites attention quand vous envoyez un nouveau theme sur votre site, les données de celui-ci pourront être modifiées voire même supprimées."
show:
title: Import en cours
help: "Votre site est en train d'être mis à jour à partir du fichier zip précédemment envoyé sur le serveur. Cette opération peut durer quelques secondes."
steps:
site: Informations sur le site
content_types: Modèles de données personnalisés
assets: Fichiers du thème
asset_collections: Collections de média
snippets: Snippets
pages: Pages
messages:
success: "Votre site a été mis à jour avec succès."
failure: "L'import n'a pas fonctionné."
formtastic: formtastic:
titles: titles:
information: Informations générales information: Informations générales
@ -260,6 +284,7 @@ fr:
other_fields: Autres informations other_fields: Autres informations
presentation: Présentation presentation: Présentation
attributes: Propriétés attributes: Propriétés
upload: Envoi au serveur
labels: labels:
theme_asset: theme_asset:
new: new:
@ -269,6 +294,9 @@ fr:
custom_fields: custom_fields:
field: field:
_alias: Alias _alias: Alias
import:
new:
source: Fichier
hints: hints:
page: page:
@ -289,3 +317,5 @@ fr:
field: field:
_alias: "Champ utilisable dans les templates liquid" _alias: "Champ utilisable dans les templates liquid"
hint: "Texte affiché dans le formulaire de l'élément juste en dessous du champ." hint: "Texte affiché dans le formulaire de l'élément juste en dessous du champ."
import:
source: "Un fichier zip contenant database.yml, les fichiers du thème et les templates de page"

View File

@ -107,4 +107,9 @@ en:
cross_domain_sessions: cross_domain_sessions:
create: create:
alert: "You need to sign in" alert: "You need to sign in"
imports:
create:
notice: "Your site is being updated."
alert: "The import was not done."

View File

@ -108,3 +108,8 @@ fr:
cross_domain_sessions: cross_domain_sessions:
create: create:
alert: "Vous devez vous authentifier" alert: "Vous devez vous authentifier"
imports:
create:
notice: "Votre site est en train d'être mis à jour"
alert: "L'import n'a pas pu se faire"

View File

@ -29,7 +29,9 @@ Rails.application.routes.draw do
resources :memberships resources :memberships
resources :theme_assets resources :theme_assets do
get :all, :action => 'index', :on => :collection, :defaults => { :all => true }
end
resources :asset_collections resources :asset_collections
@ -48,6 +50,8 @@ Rails.application.routes.draw do
resources :custom_fields, :path => 'custom/:parent/:slug/fields' resources :custom_fields, :path => 'custom/:parent/:slug/fields'
resources :cross_domain_sessions, :only => [:new, :create] resources :cross_domain_sessions, :only => [:new, :create]
resource :import, :only => [:new, :show, :create]
end end
# sitemap # sitemap

View File

@ -1,27 +1,17 @@
BOARD: BOARD:
- inline editing (http://www.aloha-editor.com/wiki/index.php/Aloha_PHP_Example) - inline editing (http://www.aloha-editor.com/wiki/index.php/Aloha_PHP_Example)
x spinner
x save automatically (callback) => store modifications
x admin buttons
x edit page
x save / cancel
x back to back-office => admin settings of the page
(- duplicate page ?)
(- super bonus statistics)
x locale
x store page toolbar status in cookie
x trim short text content
x namespace js functions
- html view in the aloha popup - html view in the aloha popup
- editable elements should wrap a tag: div, h1, ...etc (default span) - 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 ? - edit images (upload new ones, ...etc) => wait for aloha or send them an email ?
x customize tinyMCE: no html popup => div popup, nice icons
x add images / files inside long text element (back-office side at first ?) - import tool:
- add samples option
- remove existing pages / contents option
- global regions: keyword in editable element (http://www.mongodb.org/display/DOCS/Updating) - global regions: keyword in editable element (http://www.mongodb.org/display/DOCS/Updating)
- create a repo for a tool "a la" vision
- write my first tutorial about locomotive - write my first tutorial about locomotive
- asset collections => liquid
- refactor slugify method (use parameterize + create a module) - refactor slugify method (use parameterize + create a module)
- [content types] the "display column" selector should not include file types - [content types] the "display column" selector should not include file types
@ -42,6 +32,7 @@ BUGS:
- custom fields: accepts_nested_attributes weird behaviour when creating new content type + adding random fields - custom fields: accepts_nested_attributes weird behaviour when creating new content type + adding random fields
NICE TO HAVE: NICE TO HAVE:
- import / export site
- asset collections: custom resizing if image - asset collections: custom resizing if image
- super_finder - super_finder
- better icons for mime type - better icons for mime type
@ -51,7 +42,6 @@ NICE TO HAVE:
- page with regexp url ? - page with regexp url ?
- page redirection (option) - page redirection (option)
- automatic update ! - automatic update !
- import / export site
- page not found (front) => if logged in, link to create the page - page not found (front) => if logged in, link to create the page
DONE: DONE:
@ -107,4 +97,42 @@ x editable_file tag
x stylish file field x stylish file field
x remove not used editable element all in once x remove not used editable element all in once
x default content from parent editable element x default content from parent editable element
x unable to upload/remove editable file x unable to upload/remove editable file
x customize tinyMCE: no html popup => div popup, nice icons
x add images / files inside long text element (back-office side at first ?)
x create a repo for a tool "a la" vision
x asset collections => liquid
x images tag to write
! apply http://github.com/flori/json/commit/2c0f8d2c9b15a33b8d10ffcb1959aef54d320b57
x snippet dependencies => do not work correctly
? google analytics tag
x mask internal asset_collections
x refactor ui for the theme assets page
x fix assets liquid tags / filters
x upload and insert new images in a css or js from the ui is broken
x proxy for fonts (http://markevans.github.com/dragonfly/file.Rails3.html)
x order yaml file (http://www.ruby-forum.com/topic/120295)
x fix tests
x inline editing (http://www.aloha-editor.com/wiki/index.php/Aloha_PHP_Example)
x spinner
x save automatically (callback) => store modifications
x admin buttons
x edit page
x save / cancel
x back to back-office => admin settings of the page
(- duplicate page ?)
(- super bonus statistics)
x locale
x store page toolbar status in cookie
x trim short text content
x namespace js functions
x import tool:
x select field (see custom fields and nocoffee theme) ?
x disable sub tasks by passing options
x exceptions
x page to import theme
x contents: group_by, oder_by, api_enabled
x folders for theme assets
x theme assets whitelist
x fonts
x asset collections

View File

@ -1,7 +1,6 @@
require 'mimetype_fu' require 'mimetype_fu'
# require 'locomotive/patches'
require 'locomotive/version' require 'locomotive/version'
require 'locomotive/core_ext'
require 'locomotive/configuration' require 'locomotive/configuration'
require 'locomotive/logger' require 'locomotive/logger'
require 'locomotive/liquid' require 'locomotive/liquid'
@ -15,6 +14,9 @@ require 'locomotive/admin_responder'
require 'locomotive/routing' require 'locomotive/routing'
require 'locomotive/regexps' require 'locomotive/regexps'
require 'locomotive/render' require 'locomotive/render'
require 'locomotive/import'
require 'locomotive/delayed_job'
require 'locomotive/middlewares'
require 'locomotive/session_store' require 'locomotive/session_store'

View File

@ -3,6 +3,5 @@ require 'locomotive/carrierwave/base'
require 'locomotive/carrierwave/patches' require 'locomotive/carrierwave/patches'
# register missing mime types # register missing mime types
EXTENSIONS[:eot] = 'application/vnd.ms-fontobject'
# what does this do? EXTENSIONS[:woff] = 'application/x-woff'
# EXTENSIONS[:eot] = 'application/vnd.ms-fontobject'

View File

@ -39,17 +39,21 @@ module CarrierWave
module Mongoid module Mongoid
def validates_integrity_of(*attrs) def validates_integrity_of(*attrs)
options = attrs.last.is_a?(Hash) ? attrs.last : {} options = attrs.last.is_a?(Hash) ? attrs.last : {}
options[:message] ||= I18n.t('carrierwave.errors.integrity', :default => 'is not an allowed type of file.')
validates_each(*attrs) do |record, attr, value| validates_each(*attrs) do |record, attr, value|
record.errors.add attr, options[:message] if record.send("#{attr}_integrity_error") if record.send("#{attr}_integrity_error")
message = options[:message] || I18n.t('carrierwave.errors.integrity', :default => 'is not an allowed type of file.')
record.errors.add attr, message
end
end end
end end
def validates_processing_of(*attrs) def validates_processing_of(*attrs)
options = attrs.last.is_a?(Hash) ? attrs.last : {} options = attrs.last.is_a?(Hash) ? attrs.last : {}
options[:message] ||= I18n.t('carrierwave.errors.processing', :default => 'failed to be processed.')
validates_each(*attrs) do |record, attr, value| validates_each(*attrs) do |record, attr, value|
record.errors.add attr, options[:message] if record.send("#{attr}_processing_error") if record.send("#{attr}_processing_error")
message = options[:message] || I18n.t('carrierwave.errors.processing', :default => 'failed to be processed.')
record.errors.add attr, message
end
end end
end end
end end

View File

@ -29,6 +29,10 @@ class String
replace(self.slugify(options)) replace(self.slugify(options))
end end
def parameterize!(sep = '_')
replace(self.parameterize(sep))
end
end end
## Hash ## Hash

View File

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

View File

@ -0,0 +1,47 @@
require 'delayed_job'
module Delayed
module Backend
module Base
module ClassMethods
# Add a job to the queue
def enqueue(*args)
object = args.shift
unless object.respond_to?(:perform)
raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
end
attributes = {
:job_type => object.class.name.demodulize.underscore,
:payload_object => object,
:priority => Delayed::Worker.default_priority,
:run_at => nil
}
if args.first.respond_to?(:[])
attributes.merge!(args.first)
else
attributes.merge!({
:priority => args.first || Delayed::Worker.default_priority,
:run_at => args[1]
})
end
self.create(attributes)
end
end
def failed?
failed_at.present?
end
end
module Mongoid
class Job
field :job_type
field :step
referenced_in :site
end
end
end
end

7
lib/locomotive/import.rb Normal file
View File

@ -0,0 +1,7 @@
require 'locomotive/import/job'
require 'locomotive/import/site'
require 'locomotive/import/assets'
require 'locomotive/import/asset_collections'
require 'locomotive/import/content_types'
require 'locomotive/import/snippets'
require 'locomotive/import/pages'

View File

@ -0,0 +1,53 @@
module Locomotive
module Import
module AssetCollections
def self.process(context)
site, database = context[:site], context[:database]
asset_collections = database['site']['asset_collections']
return if asset_collections.nil?
asset_collections.each do |name, attributes|
puts "....asset_collection = #{attributes['slug']}"
asset_collection = site.asset_collections.where(:slug => attributes['slug']).first
asset_collection ||= self.build_asset_collection(site, attributes.merge(:name => name))
self.add_or_update_fields(asset_collection, attributes['fields'])
asset_collection.save!
site.reload
end
end
def self.build_asset_collection(site, data)
attributes = { :internal => false }.merge(data)
attributes.delete_if { |name, value| %w{fields assets}.include?(name) }
site.asset_collections.build(attributes)
end
def self.add_or_update_fields(asset_collection, fields)
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
end
end
end

View File

@ -0,0 +1,73 @@
module Locomotive
module Import
module Assets
def self.process(context)
site, theme_path = context[:site], context[:theme_path]
whitelist = self.build_regexps_in_withlist(context[:database]['site']['assets']['whitelist']) rescue nil
self.add_theme_assets(site, theme_path, whitelist)
self.add_other_assets(site, theme_path)
end
def self.add_theme_assets(site, theme_path, 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'), ''))
folder = asset_path.gsub(File.join(theme_path, 'public'), '').gsub(File.basename(asset_path), '').gsub(/^\//, '').gsub(/\/$/, '')
asset = site.theme_assets.where(:local_path => File.join(folder, File.basename(asset_path))).first
asset ||= site.theme_assets.build(:folder => folder)
asset.attributes = { :source => File.open(asset_path), :performing_plain_text => false, :hidden => !visible }
asset.save!
site.reload
end
end
end
def self.add_other_assets(site, theme_path)
collection = AssetCollection.find_or_create_internal(site)
Dir[File.join(theme_path, 'public', 'samples', '*')].each do |asset_path|
next if File.directory?(asset_path)
name = File.basename(asset_path, File.extname(asset_path)).parameterize('_')
collection.assets.create! :name => name, :source => File.open(asset_path)
end
end
def self.build_regexps_in_withlist(rules)
rules.collect do |rule|
if rule.start_with?('^')
Regexp.new(rule.gsub('/', '\/'))
else
rule
end
end
end
def self.check_against_whitelist(whitelist, path)
(whitelist || []).each do |rule|
case rule
when Regexp
return true if path =~ rule
when String
return true if path == rule
end
end
false
end
end
end
end

View File

@ -0,0 +1,84 @@
module Locomotive
module Import
module ContentTypes
def self.process(context)
site, database = context[:site], context[:database]
content_types = database['site']['content_types']
return if content_types.nil?
content_types.each do |name, attributes|
puts "....content_type = #{attributes['slug']}"
content_type = site.content_types.where(:slug => attributes['slug']).first
content_type ||= self.build_content_type(site, attributes.merge(:name => name))
self.add_or_update_fields(content_type, attributes['fields'])
self.set_highlighted_field_name(content_type)
self.set_order_by_value(content_type)
self.set_group_by_value(content_type)
content_type.save!
site.reload
end
end
def self.build_content_type(site, 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) }
site.content_types.build(attributes)
end
def self.add_or_update_fields(content_type, fields)
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 = content_type.content_custom_fields.detect { |f| f._alias == attributes[:_alias] }
field ||= content_type.content_custom_fields.build(attributes)
field.send(:set_unique_name!) if field.new_record?
field.attributes = attributes
end
end
def self.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)
order_by = (case content_type.order_by
when 'manually', '_position_in_list' then '_position_in_list'
when 'date', 'updated_at' then 'updated_at'
else
content_type.content_custom_fields.detect { |f| f._alias == content_type.order_by }._name rescue nil
end)
content_type.order_by = order_by || '_position_in_list'
end
def self.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 }
content_type.group_by_field_name = field._name if field
end
end
end
end

View File

@ -0,0 +1,73 @@
require 'zip/zipfilesystem'
module Locomotive
module Import
class Job
def initialize(theme_file, site = nil, enabled = {})
raise "Theme zipfile not found" unless File.exists?(theme_file)
@theme_file = theme_file
@site = site
@enabled = enabled
end
def before(worker)
@worker = worker
end
def perform
puts "theme_file = #{@theme_file} / #{@site.present?} / #{@enabled.inspect}"
self.unzip!
raise "No database.yml found in the theme zipfile" if @database.nil?
context = {
:database => @database,
:site => @site,
:theme_path => @theme_path,
:error => nil,
:worker => @worker
}
%w(site content_types assets asset_collections snippets pages).each do |step|
if @enabled[step] != false
"Locomotive::Import::#{step.camelize}".constantize.process(context)
@worker.update_attributes :step => step if @worker
else
puts "skipping #{step}"
end
end
end
protected
def unzip!
Zip::ZipFile.open(@theme_file) do |zipfile|
destination_path = File.join(Rails.root, 'tmp', 'themes', @site.id.to_s)
FileUtils.rm_r destination_path, :force => true
zipfile.each do |entry|
next if entry.name =~ /__MACOSX/
if entry.name =~ /database.yml$/
@database = YAML.load(zipfile.read(entry.name))
@theme_path = File.join(destination_path, entry.name.gsub('database.yml', ''))
next
end
FileUtils.mkdir_p(File.dirname(File.join(destination_path, entry.name)))
zipfile.extract(entry, File.join(destination_path, entry.name))
end
end
end
end
end
end

View File

@ -0,0 +1,132 @@
module Locomotive
module Import
module Pages
def self.process(context)
site, pages, theme_path = context[:site], context[:database]['pages'], context[:theme_path]
context[:done] = {} # initialize the hash storing pages already processed
self.add_index_and_404(context)
Dir[File.join(theme_path, 'templates', '**/*')].each do |template_path|
fullpath = template_path.gsub(File.join(theme_path, 'templates'), '').gsub('.liquid', '').gsub(/^\//, '')
# puts "=========== #{fullpath} ================="
next if %w(index 404).include?(fullpath)
self.add_page(fullpath, context)
end
end
def self.add_page(fullpath, context)
puts "....adding #{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)
parent = self.find_parent(fullpath, context)
# puts "updating..... #{fullpath} / #{template}"
page = site.pages.where(:fullpath => fullpath).first || site.pages.build
attributes = {
:title => fullpath.split('/').last.humanize,
:slug => fullpath.split('/').last,
:parent => parent,
:raw_template => template
}.merge(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
end
page.attributes = attributes
# do not parse liquid templates now
# page.instance_variable_set(:@template_changed, false)
page.save!
site.reload
context[:done][fullpath] = page
page
end
def self.build_parent_template(template, context)
# puts "building parent_template #{template.blank?}"
# just check if the template contains the extends keyword
# template
fullpath = template.scan(/\{% extends (\w+) %\}/).flatten.first
if fullpath # inheritance detected
fullpath.gsub!("'", '')
# puts "found parent_template #{fullpath}"
return if fullpath == 'parent'
self.add_page(fullpath, context)
else
# puts "no parent_template found #{fullpath}"
end
end
def self.find_parent(fullpath, context)
# puts "finding parent for #{fullpath}"
site = context[:site]
segments = fullpath.split('/')
return site.pages.index.first if segments.size == 1
segments.pop
parent_fullpath = segments.join('/').gsub(/^\//, '')
# look for a local index page in db
parent = site.pages.where(:fullpath => parent_fullpath).first
parent || self.add_page(parent_fullpath, context)
end
def self.add_index_and_404(context)
site, pages, theme_path = context[:site], context[:database]['site']['pages'], context[:theme_path]
%w(index 404).each do |slug|
page = site.pages.where({ :slug => slug, :depth => 0 }).first
# puts "building system page (#{slug}) => #{page.inspect}"
page ||= sites.pages.build(:slug => slug, :parent => nil)
template = File.read(File.join(theme_path, 'templates', "#{slug}.liquid"))
page.attributes = { :raw_template => template }.merge(pages[slug] || {})
page.save! rescue nil # TODO better error handling
site.reload
context[:done][slug] = page
end
end
end
end
end

View File

@ -0,0 +1,17 @@
module Locomotive
module Import
module Site
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) }
site.attributes = attributes
site.save!
end
end
end
end

View File

@ -0,0 +1,23 @@
module Locomotive
module Import
module Snippets
def self.process(context)
site, theme_path = context[:site], context[:theme_path]
Dir[File.join(theme_path, 'snippets', '*')].each do |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.save!
# puts "snippet = #{snippet.inspect}"
end
end
end
end
end

View File

@ -1,3 +1,5 @@
require 'locomotive/liquid/drops/base'
%w{. tags drops filters}.each do |dir| %w{. tags drops filters}.each do |dir|
Dir[File.join(File.dirname(__FILE__), 'liquid', dir, '*.rb')].each { |lib| require lib } Dir[File.join(File.dirname(__FILE__), 'liquid', dir, '*.rb')].each { |lib| require lib }
end end

View File

@ -0,0 +1,21 @@
module Locomotive
module Liquid
module Drops
class Asset < Base
def before_method(meth)
return '' if @source.nil?
if not @@forbidden_attributes.include?(meth.to_s)
value = @source.send(meth)
end
end
def url
@source.source.url
end
end
end
end
end

View File

@ -4,12 +4,47 @@ module Locomotive
class AssetCollections < ::Liquid::Drop class AssetCollections < ::Liquid::Drop
def initialize(site) def before_method(meth)
@site = site collection = @context.registers[:site].asset_collections.where(:slug => meth.to_s).first
AssetCollectionProxy.new(collection)
end
end
class AssetCollectionProxy < ::Liquid::Drop
def initialize(collection)
@collection = collection
end
def first
@collection.assets.first
end
def last
@collection.assets.last
end
def each(&block)
@collection.assets.each(&block)
end
def paginate(options = {})
paginated_collection = @collection.assets.paginate(options)
{
:collection => paginated_collection,
:current_page => paginated_collection.current_page,
:previous_page => paginated_collection.previous_page,
:next_page => paginated_collection.next_page,
:total_entries => paginated_collection.total_entries,
:total_pages => paginated_collection.total_pages,
:per_page => paginated_collection.per_page
}
end end
def before_method(meth) def before_method(meth)
@site.asset_collections.where(:slug => meth.to_s) return '' if @collection.nil?
@collection.send(meth)
end end
end end

View File

@ -3,21 +3,16 @@ module Locomotive
module Drops module Drops
class Contents < ::Liquid::Drop class Contents < ::Liquid::Drop
def initialize(site)
@site = site
end
def before_method(meth) def before_method(meth)
type = @site.content_types.where(:slug => meth.to_s).first type = @context.registers[:site].content_types.where(:slug => meth.to_s).first
ProxyCollection.new(@site, type) ProxyCollection.new(type)
end end
end end
class ProxyCollection < ::Liquid::Drop class ProxyCollection < ::Liquid::Drop
def initialize(site, content_type) def initialize(content_type)
@site = site
@content_type = content_type @content_type = content_type
@collection = nil @collection = nil
end end

View File

@ -1,18 +0,0 @@
module Locomotive
module Liquid
module Drops
class Javascripts < ::Liquid::Drop
def initialize(site)
@site = site
end
def before_method(meth)
asset = @site.theme_assets.where(:content_type => 'javascript', :slug => meth.to_s).first
!asset.nil? ? asset.source.url : nil
end
end
end
end
end

View File

@ -1,19 +0,0 @@
module Locomotive
module Liquid
module Drops
class Stylesheets < ::Liquid::Drop
def initialize(site)
@site = site
end
def before_method(meth)
asset = @site.theme_assets.where(:content_type => 'stylesheet', :slug => meth.to_s).first
!asset.nil? ? asset.source.url : nil
end
end
end
end
end

View File

@ -1,19 +0,0 @@
module Locomotive
module Liquid
module Drops
class ThemeImages < ::Liquid::Drop
def initialize(site)
@site = site
end
def before_method(meth)
asset = @site.theme_assets.where(:content_type => 'image', :slug => meth.to_s).first
!asset.nil? ? asset.source.url : nil
end
end
end
end
end

View File

@ -7,7 +7,19 @@ module Locomotive
# input: url of the css file # input: url of the css file
def stylesheet_tag(input) def stylesheet_tag(input)
return '' if input.nil? return '' if input.nil?
unless input =~ /^(\/|http:)/
segments = "stylesheets/#{input}".split('/')
filename, folder = segments.pop, segments.join('/')
stylesheet = ThemeAsset.new(:site => @context.registers[:site], :folder => folder)
input = '/' + ThemeAssetUploader.new(stylesheet).store_path(filename)
end
input = "#{input}.css" unless input.ends_with?('.css') input = "#{input}.css" unless input.ends_with?('.css')
%{<link href="#{input}" media="screen" rel="stylesheet" type="text/css" />} %{<link href="#{input}" media="screen" rel="stylesheet" type="text/css" />}
end end
@ -15,16 +27,41 @@ module Locomotive
# input: url of the javascript file # input: url of the javascript file
def javascript_tag(input) def javascript_tag(input)
return '' if input.nil? return '' if input.nil?
unless input =~ /^(\/|http:)/
segments = "javascripts/#{input}".split('/')
filename, folder = segments.pop, segments.join('/')
javascript = ThemeAsset.new(:site => @context.registers[:site], :folder => folder)
input = '/' + ThemeAssetUploader.new(javascript).store_path(filename)
end
input = "#{input}.js" unless input.ends_with?('.js') input = "#{input}.js" unless input.ends_with?('.js')
%{<script src="#{input}" type="text/javascript"></script>} %{<script src="#{input}" type="text/javascript"></script>}
end end
def theme_image_url(input)
return '' if input.nil?
input = "images/#{input}" unless input.starts_with?('/')
segments = input.split('/')
filename, folder = segments.pop, segments.join('/')
image = ThemeAsset.new(:site => @context.registers[:site], :folder => folder)
'/' + ThemeAssetUploader.new(image).store_path(filename)
end
# Write an image tag # Write an image tag
# input: url of the image OR asset drop # input: url of the image OR asset drop
def image_tag(input, *args) def image_tag(input, *args)
image_options = inline_options(args_to_options(args)) image_options = inline_options(args_to_options(args))
"<img src=\"#{File.join('/', get_path_from_asset(input))}\" #{image_options}/>" "<img src=\"#{File.join('/', get_url_from_asset(input))}\" #{image_options}/>"
end end
# Embed a flash movie into a page # Embed a flash movie into a page
@ -32,7 +69,7 @@ module Locomotive
# width: width (in pixel or in %) of the embedded movie # width: width (in pixel or in %) of the embedded movie
# height: height (in pixel or in %) of the embedded movie # height: height (in pixel or in %) of the embedded movie
def flash_tag(input, *args) def flash_tag(input, *args)
path = get_path_from_asset(input) path = get_url_from_asset(input)
embed_options = inline_options(args_to_options(args)) embed_options = inline_options(args_to_options(args))
%{ %{
<object #{embed_options}> <object #{embed_options}>
@ -100,9 +137,9 @@ module Locomotive
(options.stringify_keys.to_a.collect { |a, b| "#{a}=\"#{b}\"" }).join(' ') << ' ' (options.stringify_keys.to_a.collect { |a, b| "#{a}=\"#{b}\"" }).join(' ') << ' '
end end
# Get the path to be used in html tags such as image_tag, flash_tag, ...etc # Get the url to be used in html tags such as image_tag, flash_tag, ...etc
# input: url (String) OR asset drop # input: url (String) OR asset drop
def get_path_from_asset(input) def get_url_from_asset(input)
input.respond_to?(:url) ? input.url : input input.respond_to?(:url) ? input.url : input
end end
end end

View File

@ -5,7 +5,7 @@ module Locomotive
# #
# Usage: # Usage:
# #
# {% consume blog from 'http://nocoffee.tumblr.com/api/read.json?num=3' username: 'john', password: 'easy', format: 'json' %} # {% consume blog from 'http://nocoffee.tumblr.com/api/read.json?num=3' username: 'john', password: 'easy', format: 'json', expires_in: 3000 %}
# {% for post in blog.posts %} # {% for post in blog.posts %}
# {{ post.title }} # {{ post.title }}
# {% endfor %} # {% endfor %}
@ -23,6 +23,8 @@ module Locomotive
markup.scan(::Liquid::TagAttributes) do |key, value| markup.scan(::Liquid::TagAttributes) do |key, value|
@options[key] = value if key != 'http' @options[key] = value if key != 'http'
end end
@expires_in = (@options.delete('expires_in') || 0).to_i
@cache_key = Digest::SHA1.hexdigest(@target)
else else
raise ::Liquid::SyntaxError.new("Syntax Error in 'consume' - Valid syntax: consume <var> from \"<url>\" [username: value, password: value]") raise ::Liquid::SyntaxError.new("Syntax Error in 'consume' - Valid syntax: consume <var> from \"<url>\" [username: value, password: value]")
end end
@ -31,10 +33,18 @@ module Locomotive
end end
def render(context) def render(context)
context.stack do render_all_and_cache_it(context)
context.scopes.last[@target.to_s] = Locomotive::Httparty::Webservice.consume(@url, @options.symbolize_keys) end
render_all(@nodelist, context) protected
def render_all_and_cache_it(context)
Rails.cache.fetch(@cache_key, :expires_in => @expires_in) do
context.stack do
context.scopes.last[@target.to_s] = Locomotive::Httparty::Webservice.consume(@url, @options.symbolize_keys)
render_all(@nodelist, context)
end
end end
end end

View File

@ -0,0 +1,39 @@
module Liquid
module Locomotive
module Tags
class GoogleAnalytics < ::Liquid::Tag
Syntax = /(#{::Liquid::Expression}+)?/
def initialize(tag_name, markup, tokens, context)
if markup =~ Syntax
@account_id = $1.gsub('\'', '')
else
raise ::Liquid::SyntaxError.new("Syntax Error in 'google_analytics' - Valid syntax: google_analytics <account_id>")
end
super
end
def render(context)
%{
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '#{@account_id}']);
_gaq.push(['_trackPageview']);
(function() \{
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
\})();
</script>}
end
end
::Liquid::Template.register_tag('google_analytics', GoogleAnalytics)
end
end
end

View File

@ -5,7 +5,7 @@ module Locomotive
def end_tag def end_tag
super super
if !self.contains_super?(@nodelist) # then disable all editable_elements coming from the parent block too and not used if !self.contains_super?(@nodelist) # then disable all editable_elements coming from the parent block too and not used
@context[:page].disable_parent_editable_elements(@name) @context[:page].disable_parent_editable_elements(@name)
end end
@ -17,7 +17,7 @@ module Locomotive
nodelist.any? do |node| nodelist.any? do |node|
if node.is_a?(::Liquid::Variable) && node.name == 'block.super' if node.is_a?(::Liquid::Variable) && node.name == 'block.super'
true true
elsif node.respond_to?(:nodelist) && !node.is_a?(Locomotive::Liquid::Tags::InheritedBlock) elsif node.respond_to?(:nodelist) && !node.nodelist.nil? && !node.is_a?(Locomotive::Liquid::Tags::InheritedBlock)
contains_super?(node.nodelist) contains_super?(node.nodelist)
end end
end end

View File

@ -0,0 +1 @@
require 'locomotive/middlewares/fonts'

View File

@ -0,0 +1,44 @@
require 'rack/utils'
module Locomotive
module Middlewares
class Fonts
include Rack::Utils
def initialize(app, opts = {})
@app = app
@path_regexp = opts[:path] || %r{^/fonts/}
@file_server = ::Rack::File.new(opts[:root] || "#{Rails.root}/public")
@expires_in = opts[:expires_in] || 24.hour
end
def call(env)
if env["PATH_INFO"] =~ @path_regexp
site = fetch_site(env['SERVER_NAME'])
if site.nil?
@app.call(env)
else
env["PATH_INFO"] = ::File.join('/', 'sites', site.id.to_s, 'theme', env["PATH_INFO"])
response = @file_server.call(env)
response[1]['Cache-Control'] = "public; max-age=#{@expires_in}" # varnish
response
end
else
@app.call(env)
end
end
protected
def fetch_site(domain_name)
Rails.cache.fetch(domain_name, :expires_in => @expires_in) do
Site.match_domain(domain_name).first
end
end
end
end
end

View File

@ -1,8 +1,8 @@
require 'mongoid' require 'mongoid'
# require 'mongoid/document'
## various patches ## various patches
module Mongoid #:nodoc: module Mongoid #:nodoc:
module Document module Document
def update_child_with_noname(child, clear = false) def update_child_with_noname(child, clear = false)
@ -13,9 +13,21 @@ module Mongoid #:nodoc:
alias_method_chain :update_child, :noname alias_method_chain :update_child, :noname
# module ClassMethods
#
# def instantiate(attrs = nil, allocating = false) # used by carrierwave to back up the original file
# document = super
# document.send(:run_callbacks, :initialize) do
# document
# end
# end
#
# end
end end
end end
module Mongoid #:nodoc: module Mongoid #:nodoc:
module Validations #:nodoc: module Validations #:nodoc:
class AssociatedValidator < ActiveModel::EachValidator class AssociatedValidator < ActiveModel::EachValidator

View File

@ -53,11 +53,8 @@ module Locomotive
assigns = { assigns = {
'site' => current_site, 'site' => current_site,
'page' => @page, 'page' => @page,
'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new(current_site), 'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new,
'stylesheets' => Locomotive::Liquid::Drops::Stylesheets.new(current_site), 'contents' => Locomotive::Liquid::Drops::Contents.new,
'javascripts' => Locomotive::Liquid::Drops::Javascripts.new(current_site),
'images' => Locomotive::Liquid::Drops::ThemeImages.new(current_site),
'contents' => Locomotive::Liquid::Drops::Contents.new(current_site),
'current_page' => self.params[:page] 'current_page' => self.params[:page]
} }

View File

@ -81,6 +81,9 @@ end
# # empty page (imac 27'): User System Total Real # # empty page (imac 27'): User System Total Real
# Rendering page 10k times 21.390000 1.820000 23.210000 ( 24.120529) # Rendering page 10k times 21.390000 1.820000 23.210000 ( 24.120529)
# # empty page (mac mini core 2 duo / 2Go): User System Total Real
# Rendering a simple page 10k times 17.130000 0.420000 17.550000 ( 19.459768)
# # page with inherited template (imac 27'): User System Total Real # # page with inherited template (imac 27'): User System Total Real
# Rendering page 10k times 85.840000 7.600000 93.440000 ( 97.841248) # Rendering page 10k times 85.840000 7.600000 93.440000 ( 97.841248)
@ -89,3 +92,8 @@ end
# # with locomotive liquid (imac 27'): User System Total Real # # with locomotive liquid (imac 27'): User System Total Real
# Rendering page 10k times 38.750000 3.050000 41.800000 ( 42.880022) # Rendering page 10k times 38.750000 3.050000 41.800000 ( 42.880022)
# # with locomotive liquid (mac mini core 2 duo / 2Go): User System Total Real
# Rendering a complex page 10k times 30.840000 0.530000 31.370000 ( 32.847565)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,4 +1,3 @@
var foo = null;
var I18nLocale = null; var I18nLocale = null;
var CodeMirrorEditors = []; var CodeMirrorEditors = [];
@ -128,7 +127,7 @@ $(document).ready(function() {
$('code.html textarea').each(function() { addCodeMirrorEditor('liquid', $(this)); }); $('code.html textarea').each(function() { addCodeMirrorEditor('liquid', $(this)); });
// site selector // site selector
$('#site-selector').selectmenu({ style: 'dropdown', width: 300, offsetTop: 8, change: function(event, ui) { $('#site-selector').selectmenu({ style: 'dropdown', width: 395, offsetTop: 8, change: function(event, ui) {
$('#site-selector').parent().submit(); $('#site-selector').parent().submit();
} }); } });

View File

@ -0,0 +1,32 @@
$(document).ready(function() {
$('#import-steps').smartupdater({
url : $('#import-steps').attr('data-url'),
dataType: 'json',
minTimeout: 100
}, function(data) {
var steps = ['site', 'content_types', 'assets', 'asset_collections', 'snippets', 'pages'];
var currentIndex = data.step == 'done' ? steps.length - 1 : steps.indexOf(data.step);
for (var i = 0; i < steps.length; i++) {
var state = null;
if (i <= currentIndex) state = 'done';
if (i == currentIndex + 1 && data.failed) state = 'failed';
if (state != null)
$('#import-steps li:eq(' + i + ')').addClass(state);
}
if (data.step == 'done')
$.growl('notice', $('#import-steps').attr('data-success-message'));
if (data.failed)
$.growl('alert', $('#import-steps').attr('data-failure-message'));
if (data.step == 'done' || data.failed)
$('#import-steps').smartupdaterStop();
});
});

View File

@ -80,7 +80,7 @@ $(document).ready(function() {
if (typeof $.fn.imagepicker != 'undefined') if (typeof $.fn.imagepicker != 'undefined')
$('a#image-picker-link').imagepicker({ $('a#image-picker-link').imagepicker({
insertFn: function(link) { insertFn: function(link) {
return "{{ theme_images." + link.attr('data-slug') + " }}"; return "{{ '" + link.attr('data-local-path') + "' | theme_image_url }}";
} }
}); });

View File

@ -47,13 +47,13 @@ $.fn.imagepicker = function(options) {
.insertBefore($('.asset-picker ul li.clear')) .insertBefore($('.asset-picker ul li.clear'))
.addClass('asset'); .addClass('asset');
asset.find('h4 a').attr('href', json.url) asset.find('strong a').attr('href', json.url)
.attr('data-slug', json.slug) .attr('data-local-path', json.local_path)
.attr('data-shortcut-url', json.shortcut_url) .html(json.local_path).bind('click', function(e) {
.html(json.name).bind('click', function(e) {
copyLinkToEditor($(this), e); copyLinkToEditor($(this), e);
}); });
asset.find('.image .inside img').attr('src', json.vignette_url); asset.find('.more .size').html(json.size);
asset.find('.more .date').html(json.date);
if ($('.asset-picker ul li.asset').length % 3 == 0) if ($('.asset-picker ul li.asset').length % 3 == 0)
asset.addClass('last'); asset.addClass('last');
@ -74,7 +74,7 @@ $.fn.imagepicker = function(options) {
'onComplete': function() { 'onComplete': function() {
setupUploader(); setupUploader();
$('ul.assets h4 a').bind('click', function(e) { copyLinkToEditor($(this), e); }); $('ul.theme-assets strong a').bind('click', function(e) { copyLinkToEditor($(this), e); });
} }
}); });
}); });

View File

@ -0,0 +1,155 @@
/**
* smartupdater - jQuery Plugin
*
* Version - 2.0.0
*
* Copyright (c) 2010 Vadim Kiryukhin
* vkiryukhin@gmail.com
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
* Based on the work done by Terry M. Schmidt and the jQuery ekko plugin.
*
* USAGE:
*
* $("#myObject").smartupdater({
* url : "demo.php",
* minTimeout : 60000
* }, function (data) {
* //process data here;
* }
* );
*
* Public functions:
* $("#myObject").smartupdaterStop();
* $("#myObject").smartupdaterRestart();
* $("#myObject").smartupdaterSetTimeout();
*
* Public Attributes:
* var smStatus = $("#myObject")[0].smartupdaterStatus.state; // "ON" | "OFF" | "undefined"
* var smTimeout = $("#myObject")[0].smartupdaterStatus.timeout; // current timeout
*
**/
(function(jQuery) {
jQuery.fn.smartupdater = function (options, callback) {
return this.each(function () {
var elem = this;
elem.settings = jQuery.extend({
url : '', // see jQuery.ajax for details
type : 'get', // see jQuery.ajax for details
data : '', // see jQuery.ajax for details
dataType : 'text', // see jQuery.ajax for details
minTimeout : 60000, // Starting value for the timeout in milliseconds; default 1 minute.
maxTimeout : ((1000 * 60) * 60), // Default 1 hour.
multiplier : 2, //if set to 2, interval will double each time the response hasn't changed.
maxFailedRequests : 10 // smartupdater stops after this number of consecutive ajax failures;
}, options);
elem.smartupdaterStatus = {};
elem.smartupdaterStatus.state = '';
elem.smartupdaterStatus.timeout = 0;
var es = elem.settings;
es.prevContent = '';
es.originalMinTimeout = es.minTimeout;
es.failedRequests = 0;
es.response = '';
function start() {
$.ajax({url: es.url,
type: es.type,
data: es.data,
dataType: es.dataType,
success: function (data) {
if(es.dataType == 'json') {
es.response = JSON.stringify(data);
if ( data.smartupdater) {
es.originalMinTimeout = data.smartupdater;
}
} else {
es.response = data;
}
if (es.prevContent != es.response) {
es.prevContent = es.response;
es.minTimeout = es.originalMinTimeout;
es.periodicalUpdater = setTimeout(start, es.minTimeout);
elem.smartupdaterStatus.timeout = es.minTimeout;
callback(data);
} else if (es.multiplier > 1) {
es.minTimeout = (es.minTimeout < es.maxTimeout) ? Math.round(es.minTimeout * es.multiplier) : es.maxTimeout;
es.periodicalUpdater = setTimeout(start, es.minTimeout);
elem.smartupdaterStatus.timeout = es.minTimeout;
}
es.failedRequests = 0;
elem.smartupdaterStatus.state = 'ON';
},
error: function() {
if ( ++es.failedRequests < es.maxFailedRequests ) {
es.periodicalUpdater = setTimeout(start, es.minTimeout);
elem.smartupdaterStatus.timeout = es.minTimeout;
} else {
clearTimeout(es.periodicalUpdater);
elem.smartupdaterStatus.state = 'OFF';
}
}
})
}
es.fnStart = start;
start();
});
};
jQuery.fn.smartupdaterStop = function () {
return this.each(function () {
var elem = this;
clearTimeout(elem.settings.periodicalUpdater);
elem.smartupdaterStatus.state = 'OFF';
});
};
jQuery.fn.smartupdaterRestart = function () {
return this.each(function () {
var elem = this;
clearTimeout(elem.settings.periodicalUpdater);
elem.settings.minTimeout = elem.settings.originalMinTimeout;
elem.settings.fnStart();
});
};
jQuery.fn.smartupdaterSetTimeout = function (period) {
return this.each(function () {
var elem = this;
clearTimeout(elem.settings.periodicalUpdater);
this.settings.originalMinTimeout = period;
this.settings.fnStart();
});
};
})(jQuery);
/******************************************************
* http://www.JSON.org/json2.js
* 2010-03-20
*
* Public Domain.
*
* NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
*
* See http://www.JSON.org/js.html
*********************************************************/
if(!this.JSON){this.JSON={}}(function(){function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i<length;i+=1){partial[i]=str(i,value)||"null"}v=partial.length===0?"[]":gap?"[\n"+gap+partial.join(",\n"+gap)+"\n"+mind+"]":"["+partial.join(",")+"]";gap=mind;return v}if(rep&&typeof rep==="object"){length=rep.length;for(i=0;i<length;i+=1){k=rep[i];if(typeof k==="string"){v=str(k,value);if(v){partial.push(quote(k)+(gap?": ":":")+v)}}}}else{for(k in value){if(Object.hasOwnProperty.call(value,k)){v=str(k,value);if(v){partial.push(quote(k)+(gap?": ":":")+v)}}}}v=partial.length===0?"{}":gap?"{\n"+gap+partial.join(",\n"+gap)+"\n"+mind+"}":"{"+partial.join(",")+"}";gap=mind;return v}}if(typeof JSON.stringify!=="function"){JSON.stringify=function(value,replacer,space){var i;gap="";indent="";if(typeof space==="number"){for(i=0;i<space;i+=1){indent+=" "}}else{if(typeof space==="string"){indent=space}}rep=replacer;if(replacer&&typeof replacer!=="function"&&(typeof replacer!=="object"||typeof replacer.length!=="number")){throw new Error("JSON.stringify")}return str("",{"":value})}}if(typeof JSON.parse!=="function"){JSON.parse=function(text,reviver){var j;function walk(holder,key){var k,v,value=holder[key];if(value&&typeof value==="object"){for(k in value){if(Object.hasOwnProperty.call(value,k)){v=walk(value,k);if(v!==undefined){value[k]=v}else{delete value[k]}}}}return reviver.call(holder,key,value)}text=String(text);cx.lastIndex=0;if(cx.test(text)){text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})}if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,""))){j=eval("("+text+")");return typeof reviver==="function"?walk({"":j},""):j}throw new SyntaxError("JSON.parse")}}}());

View File

@ -37,7 +37,7 @@ $(document).ready(function() {
$('a#image-picker-link').imagepicker({ $('a#image-picker-link').imagepicker({
insertFn: function(link) { insertFn: function(link) {
return link.attr('data-shortcut-url'); return link.attr('href');
} }
}); });
}); });

View File

@ -53,7 +53,6 @@ ul.list li {
margin-bottom: 10px; margin-bottom: 10px;
position: relative; position: relative;
clear: both; clear: both;
background: transparent url(/images/admin/list/item.png) no-repeat 0 0;
} }
ul.list li em { ul.list li em {
@ -64,6 +63,13 @@ ul.list li em {
width: 18px; width: 18px;
} }
ul.list li strong {
margin-left: 18px;
display: block;
height: 31px;
background: transparent url(/images/admin/list/item-right.png) no-repeat right 0;
}
ul.list li strong a { ul.list li strong a {
position: relative; position: relative;
top: 2px; top: 2px;
@ -71,6 +77,7 @@ ul.list li strong a {
text-decoration: none; text-decoration: none;
color: #1f82bc; color: #1f82bc;
font-size: 0.9em; font-size: 0.9em;
text-shadow: 1px 1px 1px #fff;
} }
ul.list.sortable li strong a { left: 10px; } ul.list.sortable li strong a { left: 10px; }
@ -104,6 +111,12 @@ div#asset-uploader { display: inline-block; margin-left: 10px; }
div#asset-uploader span.spinner { position: relative; top: -3px; display: none; } div#asset-uploader span.spinner { position: relative; top: -3px; display: none; }
div#uploadAssetsInputQueue { display: none; } div#uploadAssetsInputQueue { display: none; }
/* ___ theme assets ___ */
ul.theme-assets { margin-left: 40px; }
ul.theme-assets li.hidden strong a { font-style: italic; color: #8B8D9A; font-weight: normal; }
/* ___ contents ___ */ /* ___ contents ___ */
#contents-list li { background: none; } #contents-list li { background: none; }
@ -120,6 +133,7 @@ div#uploadAssetsInputQueue { display: none; }
background: transparent url(/images/admin/list/item-right.png) no-repeat right 0; background: transparent url(/images/admin/list/item-right.png) no-repeat right 0;
} }
/* ___ snippets ___ */
/* ___ pages ___ */ /* ___ pages ___ */
@ -176,6 +190,7 @@ div#uploadAssetsInputQueue { display: none; }
color: #1f82bc; color: #1f82bc;
font-size: 0.9em; font-size: 0.9em;
padding-left: 6px; padding-left: 6px;
text-shadow: 1px 1px 1px #fff;
} }
#pages-list li strong a:hover { text-decoration: underline; } #pages-list li strong a:hover { text-decoration: underline; }
@ -205,3 +220,31 @@ div#uploadAssetsInputQueue { display: none; }
#progressbar-wrapper { margin: 40px 0; height: 30px; } #progressbar-wrapper { margin: 40px 0; height: 30px; }
#progressbar-wrapper #progressbar { height: 100%; } #progressbar-wrapper #progressbar { height: 100%; }
/* ___ import steps ___ */
#import-steps { margin: 0px 200px; }
#import-steps li strong a { color: #b7baca; }
#import-steps li .more .states {
position: relative;
top: 4px;
height: 16px;
width: 16px;
background: transparent url(/images/admin/list/icons/states.png) no-repeat 0 0;
}
#import-steps li.done .more .states {
background-position: 0 -16px;
}
#import-steps li.failed .more .states {
background-position: 0 -32px;
}
#import-steps li.done strong a {
color: #1F82BC;
}

View File

@ -49,7 +49,7 @@
/* ___ asset picker ___ */ /* ___ asset picker ___ */
div.asset-picker { width: 470px; position: relative; } div.asset-picker { width: 720px; position: relative; }
div.asset-picker .actions { position: absolute; right: 4px; top: 0px; } div.asset-picker .actions { position: absolute; right: 4px; top: 0px; }
div.asset-picker p.no-items { background-image: url("/images/admin/list/none-small.png"); } div.asset-picker p.no-items { background-image: url("/images/admin/list/none-small.png"); }
@ -57,6 +57,9 @@ div.asset-picker p.no-items { background-image: url("/images/admin/list/none-sma
div.asset-picker ul { overflow: auto; height: 471px; } div.asset-picker ul { overflow: auto; height: 471px; }
div.asset-picker ul li.new-asset { display: none; } div.asset-picker ul li.new-asset { display: none; }
div.asset-picker ul { margin: 0px; }
div.asset-picker ul li .more { top: 8px; }
/* ___ custom fields ___ */ /* ___ custom fields ___ */
#edit-custom-field { #edit-custom-field {

View File

@ -7,6 +7,7 @@
padding: 0 10px; padding: 0 10px;
font-family: Helvetica; font-family: Helvetica;
-webkit-box-shadow: -3px 3px 12px #818181; -webkit-box-shadow: -3px 3px 12px #818181;
z-index: 999;
} }
#page-toolbar ul { #page-toolbar ul {
@ -26,6 +27,7 @@
padding-left: 24px; padding-left: 24px;
text-decoration: none; text-decoration: none;
color: #fff; color: #fff;
outline: none;
} }
#page-toolbar ul li.link a:hover span { text-decoration: underline; } #page-toolbar ul li.link a:hover span { text-decoration: underline; }

View File

@ -97,6 +97,7 @@ body {
font-weight: bold; font-weight: bold;
color: #1e1f26; color: #1e1f26;
padding: 7px 0 10px 20px; padding: 7px 0 10px 20px;
text-shadow: 1px 1px 1px #fff;
} }
#content div.inner p { #content div.inner p {

View File

@ -24,10 +24,10 @@ describe Locomotive::Liquid::Drops::Contents do
def render_template(template = '', assigns = {}) def render_template(template = '', assigns = {})
assigns = { assigns = {
'contents' => Locomotive::Liquid::Drops::Contents.new(@site) 'contents' => Locomotive::Liquid::Drops::Contents.new
}.merge(assigns) }.merge(assigns)
Liquid::Template.parse(template).render assigns Liquid::Template.parse(template).render(::Liquid::Context.new({}, assigns, { :site => @site }))
end end
end end

View File

@ -4,20 +4,47 @@ describe Locomotive::Liquid::Filters::Html do
include Locomotive::Liquid::Filters::Html include Locomotive::Liquid::Filters::Html
before(:each) do
@context = build_context
end
it 'should return a link tag for a stylesheet file' do it 'should return a link tag for a stylesheet file' do
result = "<link href=\"main.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />" result = "<link href=\"/sites/42/theme/stylesheets/main.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />"
stylesheet_tag('main.css').should == result stylesheet_tag('main.css').should == result
stylesheet_tag('main').should == result stylesheet_tag('main').should == result
stylesheet_tag(nil).should == '' stylesheet_tag(nil).should == ''
end end
it 'should return a link tag for a stylesheet file with folder' do
result = "<link href=\"/sites/42/theme/stylesheets/trash/main.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />"
stylesheet_tag('trash/main.css').should == result
end
it 'should return a link tag for a stylesheet file without touching the url' do
result = "<link href=\"/trash/main.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />"
stylesheet_tag('/trash/main.css').should == result
stylesheet_tag('/trash/main').should == result
end
it 'should return a script tag for a javascript file' do it 'should return a script tag for a javascript file' do
result = %{<script src="main.js" type="text/javascript"></script>} result = %{<script src="/sites/42/theme/javascripts/main.js" type="text/javascript"></script>}
javascript_tag('main.js').should == result javascript_tag('main.js').should == result
javascript_tag('main').should == result javascript_tag('main').should == result
javascript_tag(nil).should == '' javascript_tag(nil).should == ''
end end
it 'should return a script tag for a javascript file with folder' do
result = %{<script src="/sites/42/theme/javascripts/trash/main.js" type="text/javascript"></script>}
javascript_tag('trash/main.js').should == result
javascript_tag('trash/main').should == result
end
it 'should return a script tag for a javascript file without touching the url' do
result = %{<script src="/trash/main.js" type="text/javascript"></script>}
javascript_tag('/trash/main.js').should == result
javascript_tag('/trash/main').should == result
end
it 'should return an image tag without paramaters' do it 'should return an image tag without paramaters' do
image_tag('foo.jpg').should == "<img src=\"/foo.jpg\" />" image_tag('foo.jpg').should == "<img src=\"/foo.jpg\" />"
end end
@ -79,4 +106,15 @@ describe Locomotive::Liquid::Filters::Html do
html.should == '' html.should == ''
end end
def build_context
Site.any_instance.stubs(:id).returns(42)
klass = Class.new
klass.class_eval do
def registers
{ :site => Factory.build(:site) }
end
end
klass.new
end
end end

View File

@ -23,10 +23,28 @@ describe ThemeAsset do
@asset.height.should == 32 @asset.height.should == 32
end end
it 'should have a slug' do end
describe 'local path and folder' do
it 'should set the local path based on the content type' do
@asset.source = FixturedAsset.open('5k.png') @asset.source = FixturedAsset.open('5k.png')
@asset.save @asset.save
@asset.slug.should == '5k' @asset.local_path.should == 'images/5k.png'
end
it 'should set the local path based on the folder' do
@asset.folder = 'trash'
@asset.source = FixturedAsset.open('5k.png')
@asset.save
@asset.local_path.should == 'images/trash/5k.png'
end
it 'should set sanitize the local path' do
@asset.folder = '/images/à la poubelle'
@asset.source = FixturedAsset.open('5k.png')
@asset.save
@asset.local_path.should == 'images/a_la_poubelle/5k.png'
end end
end end
@ -64,10 +82,12 @@ describe ThemeAsset do
before(:each) do before(:each) do
ThemeAsset.any_instance.stubs(:site_id).returns('test') ThemeAsset.any_instance.stubs(:site_id).returns('test')
@asset = Factory.build(:theme_asset, :site => Factory.build(:site)) @asset = Factory.build(:theme_asset, {
@asset.performing_plain_text = true :site => Factory.build(:site),
@asset.slug = 'a file' :plain_text_name => 'test',
@asset.plain_text = "Lorem ipsum" :plain_text => 'Lorem ipsum',
:performing_plain_text => true
})
end end
it 'should handle stylesheet' do it 'should handle stylesheet' do
@ -84,24 +104,6 @@ describe ThemeAsset do
@asset.source.should_not be_nil @asset.source.should_not be_nil
end end
context 'shortcut urls' do
before(:each) do
@image = Factory.build(:theme_asset, :source => FixturedAsset.open('5k.png'))
@image.source.stubs(:url).returns('5k.png')
@asset.stubs(:stylesheet?).returns(true)
@asset.site.theme_assets.stubs(:where).returns([@image])
@asset.plain_text = 'body { background-image: url("/theme/images/5k.png"); } h1 { background-image: url("/images/5k.png"); }'
@asset.store_plain_text
end
it 'replaces shortcut url if present' do
@asset.plain_text.should == 'body { background-image: url("/theme/images/5k.png"); } h1 { background-image: url("/images/5k.png"); }'
@asset.source.read.should == 'body { background-image: url("5k.png"); } h1 { background-image: url("/images/5k.png"); }'
end
end
end end
end end