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
config/deploy.rb
perf/test.rb
<<<<<<< HEAD
gem_graph.png
sites/
=======
sites
>>>>>>> theme

50
Gemfile
View File

@ -1,28 +1,34 @@
# Edit this Gemfile to bundle your application's dependencies.
source :rubygems
sou rce :rubygems
# add in all the runtime dependencies
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 'rails', '>= 3.0.0'
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
@ -46,7 +52,7 @@ group :test do
gem 'capybara'
gem 'database_cleaner'
gem 'cucumber', "0.8.5"
gem 'cucumber', '0.8.5'
gem 'cucumber-rails'
gem 'spork'
gem 'launchy'

View File

@ -15,25 +15,6 @@ GIT
rspec (>= 1.3)
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
remote: http://rubygems.org/
specs:
@ -68,14 +49,14 @@ GEM
activesupport (3.0.0)
arel (1.0.1)
activesupport (~> 3.0.0)
autotest (4.3.2)
autotest (4.4.1)
aws (2.3.21)
http_connection
uuidtools
xml-simple
bcrypt-ruby (2.1.2)
bson (1.0.9)
bson_ext (1.0.9)
bson (1.1)
bson_ext (1.1)
builder (2.1.2)
capybara (0.3.9)
culerity (>= 0.2.4)
@ -84,9 +65,11 @@ GEM
rack (>= 1.0.0)
rack-test (>= 0.5.4)
selenium-webdriver (>= 0.0.3)
carrierwave (0.5.0.beta2)
activesupport (>= 3.0.0.beta4)
carrierwave (0.5.0)
activesupport (~> 3.0.0)
cgi_multipart_eof_fix (2.5.0)
childprocess (0.0.7)
ffi (~> 0.6.3)
columnize (0.3.1)
configuration (1.1.0)
crack (0.1.8)
@ -99,8 +82,18 @@ GEM
cucumber-rails (0.3.2)
cucumber (>= 0.8.0)
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)
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)
bcrypt-ruby (~> 2.1.2)
warden (~> 0.10.7)
@ -124,7 +117,7 @@ GEM
growl-glue (1.0.7)
haml (3.0.18)
has_scope (0.5.0)
heroku (1.10.8)
heroku (1.10.14)
json_pure (>= 1.2.0, < 1.5.0)
launchy (~> 0.3.2)
rest-client (>= 1.4.0, < 1.7.0)
@ -140,8 +133,13 @@ GEM
configuration (>= 0.0.5)
rake (>= 0.8.1)
linecache (0.43)
locomotive_liquid (2.1.3)
mail (2.2.6.1)
locomotive_carrierwave (0.5.0.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)
mime-types
treetop (>= 1.4.5)
@ -149,9 +147,11 @@ GEM
mimetype-fu (0.1.2)
mongo (1.0.9)
bson (>= 1.0.5)
mongoid_acts_as_tree (0.1.5)
bson (>= 0.20.1)
mongoid (<= 2.0.0)
mongoid (2.0.0.beta.19)
activemodel (~> 3.0)
mongo (= 1.0.9)
tzinfo (~> 0.3.22)
will_paginate (~> 3.0.pre)
mongrel (1.1.5)
cgi_multipart_eof_fix (>= 2.4)
daemons (>= 1.0.3)
@ -182,31 +182,32 @@ GEM
rest-client (1.6.1)
mime-types (>= 1.16)
rmagick (2.12.2)
rspec (2.0.0.beta.22)
rspec-core (= 2.0.0.beta.22)
rspec-expectations (= 2.0.0.beta.22)
rspec-mocks (= 2.0.0.beta.22)
rspec-core (2.0.0.beta.22)
rspec-expectations (2.0.0.beta.22)
rspec (2.0.0)
rspec-core (= 2.0.0)
rspec-expectations (= 2.0.0)
rspec-mocks (= 2.0.0)
rspec-core (2.0.0)
rspec-expectations (2.0.0)
diff-lcs (>= 1.1.2)
rspec-mocks (2.0.0.beta.22)
rspec-core (= 2.0.0.beta.22)
rspec-expectations (= 2.0.0.beta.22)
rspec-rails (2.0.0.beta.22)
rspec (= 2.0.0.beta.22)
rspec-mocks (2.0.0)
rspec-core (= 2.0.0)
rspec-expectations (= 2.0.0)
rspec-rails (2.0.0)
rspec (= 2.0.0)
ruby-debug (0.10.3)
columnize (>= 0.1)
ruby-debug-base (~> 0.10.3.0)
ruby-debug-base (0.10.3)
linecache (>= 0.3)
rubyzip (0.9.4)
selenium-webdriver (0.0.28)
ffi (>= 0.6.1)
selenium-webdriver (0.0.29)
childprocess (>= 0.0.7)
ffi (~> 0.6.3)
json_pure
rubyzip
spork (0.8.4)
term-ansicolor (1.0.5)
thor (0.14.2)
thor (0.14.3)
treetop (1.4.8)
polyglot (>= 0.3.1)
trollop (1.16.2)
@ -226,14 +227,15 @@ DEPENDENCIES
actionmailer-with-request
autotest
aws
bson_ext (>= 1.0.8)
bson_ext (= 1.1)
capybara
carrierwave (= 0.5.0.beta2)
cgi_multipart_eof_fix
cucumber (= 0.8.5)
cucumber-rails
custom_fields!
custom_fields (= 1.0.0.beta)
database_cleaner
delayed_job (= 2.1.0.pre2)
delayed_job_mongoid (= 1.0.0.rc)
devise (= 1.1.2)
factory_girl_rails
fastthread
@ -244,16 +246,18 @@ DEPENDENCIES
httparty (>= 0.6.1)
inherited_resources (>= 1.1.2)
launchy
locomotive_liquid (>= 2.1.3)
locomotive_carrierwave
locomotive_liquid (= 2.2.2)
locomotive_mongoid_acts_as_tree (= 0.1.5.1)
mimetype-fu
mocha!
mongoid!
mongoid_acts_as_tree (= 0.1.5)
mongoid (= 2.0.0.beta.19)
mongrel
pickle!
rails (>= 3.0.0)
rmagick (= 2.12.2)
rspec-rails (>= 2.0.0.beta.18)
ruby-debug
rubyzip
spork
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)
* flexible content types
* playing smoothly with Heroku and MongoHQ
* inline editing (coming soon)
* inline editing (beta)
h2. Strategy / Development status
@ -19,12 +19,13 @@ h2. Gems
Here is a short list of main gems used in the application.
* Rails 3 RC
* Mongoid 2.0.0.beta 16 (with MongoDB 1.6)
* Rails 3.0
* Mongoid 2.0.0.beta 17 (with MongoDB 1.6)
* Liquid
* Devise
* Carrierwave
* Haml
* Delayed job
h2. Installation

View File

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

View File

@ -3,5 +3,9 @@ module Admin
sections 'contents'
def destroy
destroy! { admin_pages_url }
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
class SnippetsController < BaseController
sections 'settings'
sections 'settings', 'theme_assets'
respond_to :json, :only => :update
def index
@snippets = current_site.snippets.order_by([[:name, :asc]])
def destroy
destroy! do |format|
format.html { redirect_to admin_theme_assets_url }
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

@ -26,16 +26,21 @@ class Snippet
protected
def normalize_slug
# TODO: refactor it
self.slug = self.name.clone if self.slug.blank? && self.name.present?
self.slug.slugify!(:without_extension => true, :downcase => true) if self.slug.present?
end
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.each do |page|
self._change_snippet_inside_template(page.template.root)
page.instance_variable_set(:@template_changed, true)
page.send(:_serialize_template) && page.save
end
end
@ -44,11 +49,11 @@ class Snippet
case node
when Locomotive::Liquid::Tags::Snippet
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
else
if node.respond_to?(:nodelist)
node.nodelist.each do |child|
(node.nodelist || []).each do |child|
self._change_snippet_inside_template(child)
end
end

View File

@ -2,16 +2,14 @@ class ThemeAsset
include Locomotive::Mongoid::Document
## Extensions ##
include Models::Extensions::Asset::Vignette
## fields ##
field :slug
field :local_path
field :content_type
field :width, :type => Integer
field :height, :type => Integer
field :size, :type => Integer
field :plain_text
field :folder, :default => nil
field :hidden, :type => Boolean, :default => false
mount_uploader :source, ThemeAssetUploader
## associations ##
@ -19,22 +17,25 @@ class ThemeAsset
## indexes ##
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 ##
before_validation :sanitize_slug
before_validation :store_plain_text
before_save :set_slug
before_save :sanitize_folder
before_save :build_local_path
## validations ##
validate :extname_can_not_be_changed
validates_presence_of :site, :source
validates_presence_of :slug, :if => Proc.new { |a| a.new_record? && a.performing_plain_text? }
validates_uniqueness_of :slug, :scope => [:site_id, :content_type]
validates_presence_of :plain_text_name, :if => Proc.new { |a| a.performing_plain_text? }
validates_uniqueness_of :local_path, :scope => :site_id
validates_integrity_of :source
validate :content_type_can_not_changed
## named scopes ##
scope :visible, lambda { |all| all ? {} : { :where => { :hidden => false } } }
## accessors ##
attr_accessor :performing_plain_text
attr_accessor :plain_text_name, :plain_text, :performing_plain_text
## methods ##
@ -48,77 +49,90 @@ class ThemeAsset
self.stylesheet? || self.javascript?
end
def plain_text
if self.stylesheet_or_javascript?
self.plain_text = self.source.read if read_attribute(:plain_text).blank?
read_attribute(:plain_text)
def local_path(short = false)
if short
self.read_attribute(:local_path).gsub(/^#{self.content_type.pluralize}\//, '')
else
nil
self.read_attribute(:local_path)
end
end
def plain_text=(source)
self.performing_plain_text = true if self.performing_plain_text.nil?
write_attribute(:plain_text, source)
def plain_text_name
if not @plain_text_name_changed
@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
def performing_plain_text?
return true if !self.new_record? && self.stylesheet_or_javascript? && self.errors.empty?
!(self.performing_plain_text.blank? || self.performing_plain_text == 'false' || self.performing_plain_text == false)
Boolean.set(self.performing_plain_text) || false
end
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
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]
return if !self.stylesheet_or_javascript? || self.plain_text_name.blank? || data.blank?
content_type = content_type.singularize
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
sanitized_source = self.escape_shortcut_urls(data)
self.source = CarrierWave::SanitizedFile.new({
:tempfile => StringIO.new(sanitized_source),
:filename => "#{self.slug}.#{self.stylesheet? ? 'css' : 'js'}"
:filename => "#{self.plain_text_name}.#{self.stylesheet? ? 'css' : 'js'}"
})
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
{ :url => self.source.url }.merge(self.attributes)
end
protected
def sanitize_slug
self.slug.slugify!(:underscore => true) if self.slug.present?
def safe_source_filename
self.source_filename || self.source.send(:original_filename) rescue nil
end
def set_slug
if self.slug.blank?
self.slug = File.basename(self.source_filename, File.extname(self.source_filename))
self.sanitize_slug
def sanitize_folder
self.folder = self.content_type.pluralize if self.folder.blank?
# 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
def extname_can_not_be_changed
return if self.new_record?
def build_local_path
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)
self.errors.add(:source, :extname_changed)
def escape_shortcut_urls(text)
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
def content_type_can_not_changed
self.errors.add(:source, :extname_changed) if !self.new_record? && self.content_type_changed?
end
end

View File

@ -31,7 +31,7 @@ class AssetUploader < CarrierWave::Uploader::Base
process :set_size
process :set_width_and_height
def set_content_type
def set_content_type(*args)
value = :other
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
end
def set_size
def set_size(*args)
model.size = file.size
end
@ -61,12 +61,12 @@ class AssetUploader < CarrierWave::Uploader::Base
def self.content_types
{
: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'],
:pdf => ['application/pdf', 'application/x-pdf'],
:stylesheet => ['text/css'],
: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

View File

@ -6,27 +6,16 @@ class ThemeAssetUploader < AssetUploader
process :set_size
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
"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
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

View File

@ -4,6 +4,7 @@
= render 'admin/shared/menu/settings'
- 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'
%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
- if current_admin.sites.size > 1
= 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'
- else
= link_to current_site.name, admin_root_url, :class => 'single'

View File

@ -1,5 +1,5 @@
%ul
= 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 'account', edit_admin_my_account_url

View File

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

View File

@ -12,4 +12,4 @@
= 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 '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
%li{ :class => "#{asset.new_record? ? 'new-asset' : 'asset'} #{'last' if (asset_counter + 1) % per_row == 0}"}
%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
.image
.inside
= vignette_tag(asset)
%li{ :class => "#{asset.new_record? ? 'new-asset' : 'asset'} #{'hidden' if asset.hidden?}" }
%em
%strong= link_to asset.local_path(!edit), edit ? edit_admin_theme_asset_path(asset) : asset.source.url, :'data-local-path' => asset.local_path
.more
%span.size= number_to_human_size(asset.size)
&mdash;
%span!= t('.updated_at')
%span.date= l asset.updated_at, :format => :short
- if edit
.actions
= link_to image_tag('admin/list/icons/cross.png'), admin_theme_asset_path(asset), :class => 'remove', :confirm => t('admin.messages.confirm'), :method => :delete
= 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
#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.input :source
= f.input :slug
- if @theme_asset.new_record? || @theme_asset.stylesheet_or_javascript?
%span.alt
!= t('admin.theme_assets.form.choose_plain_text')
- 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
- if @theme_asset.new_record?
= f.input :slug
= f.input :plain_text_name
= 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
%code{ :class => (@theme_asset.size && @theme_asset.size > 40000 ? 'nude' : (@theme_asset.content_type || 'stylesheet')) }
@ -32,9 +31,7 @@
%span.alt
!= t('admin.theme_assets.form.choose_file')
- if @theme_asset.image?
= f.foldable_inputs :name => "#{t('formtastic.titles.preview')} #{image_dimensions_and_size(@theme_asset)}", :class => 'preview' do
%li
.image
.inside
= image_tag(@theme_asset.source.url(:preview))
= f.foldable_inputs :name => :options do
= f.input :folder
= f.custom_input :hidden, :css => 'toggle' do
= f.check_box :hidden

View File

@ -6,7 +6,7 @@
- content_for :buttons do
= 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|

View File

@ -4,12 +4,12 @@
.actions
= 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')
%ul.assets
= render 'asset', :asset => current_site.theme_assets.build, :edit => false
%ul.list.theme-assets
= 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

View File

@ -4,33 +4,48 @@
= render 'admin/shared/menu/settings'
- 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'
%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')
- if @non_image_assets.empty?
- if @js_and_css_assets.empty?
%p.no-items!= t('.no_items', :url => new_admin_theme_asset_url)
- else
%ul.assets
= render :partial => 'asset', :collection => @non_image_assets
%li.clear
%ul.list.theme-assets
= render :partial => 'asset', :collection => @js_and_css_assets
%br
%h3!= t('.images')
- if @image_assets.empty?
- if @assets[:images].nil?
%p.no-items!= t('.no_items', :url => new_admin_theme_asset_url)
- else
%ul.assets
= render :partial => 'asset', :collection => @image_assets
%li.clear
%ul.list.theme-assets
= render :partial => 'asset', :collection => @assets[:images]
- if not @flash_assets.empty?
- if @assets[:fonts]
%br
%h3!= t('.flashes')
%ul.assets
= render :partial => 'asset', :collection => @flash_assets
%li.clear
%h3!= t('.fonts')
%ul.list.theme-assets
= render :partial => 'asset', :collection => @assets[:fonts]
- 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.
config.filter_parameters << :password
config.middleware.insert_after ::ActionDispatch::Static, '::Locomotive::Middlewares::Fonts', :path => %r{^/fonts}
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.uncountable %w( fish sheep )
# 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/core_ext.rb'
Locomotive.configure do |config|
# 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
create: Create
update: Update
send: Send
errors:
"500":
@ -131,6 +132,7 @@ en:
current_sites:
edit:
import: import
new_membership: add account
help: "The site name can be updated by clicking it."
ask_for_name: "Please type the new site name"
@ -156,19 +158,23 @@ en:
theme_assets:
index:
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
snippets: Snippets
css_and_js: Style and javascript
fonts: Fonts
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."
asset:
updated_at: Updated at
new:
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."
edit:
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:
picker_link: Insert a file into the code
choose_file: Choose file
@ -242,6 +248,24 @@ en:
title: Cross-domain authentication
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:
titles:
information: General information
@ -261,6 +285,7 @@ en:
other_fields: Other information
presentation: Presentation
attributes: Attributes
upload: Upload
labels:
page:
raw_template: Template
@ -272,6 +297,9 @@ en:
custom_fields:
field:
_alias: Alias
import:
new:
source: File
hints:
page:
@ -292,4 +320,6 @@ en:
field:
_alias: "Property available in liquid templates"
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
create: Créer
update: Mettre à jour
send: Envoyer
custom_fields:
edit:
@ -131,6 +132,7 @@ fr:
current_sites:
edit:
import: importer
new_membership: ajouter compte
help: "Le nom du site est modifiable en cliquant dessus."
ask_for_name: "Veuillez entrer le nouveau nom"
@ -156,13 +158,17 @@ fr:
theme_assets:
index:
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
snippets: Snippets
css_and_js: Style et javascript
images: Images
flashes: Flash
media: Media
fonts: Polices
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:
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."
@ -241,6 +247,24 @@ fr:
title: Transfert vers un autre site
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:
titles:
information: Informations générales
@ -260,6 +284,7 @@ fr:
other_fields: Autres informations
presentation: Présentation
attributes: Propriétés
upload: Envoi au serveur
labels:
theme_asset:
new:
@ -269,6 +294,9 @@ fr:
custom_fields:
field:
_alias: Alias
import:
new:
source: Fichier
hints:
page:
@ -289,3 +317,5 @@ fr:
field:
_alias: "Champ utilisable dans les templates liquid"
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

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

View File

@ -1,27 +1,17 @@
BOARD:
- 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
- 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 ?
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)
- create a repo for a tool "a la" vision
- write my first tutorial about locomotive
- asset collections => liquid
- refactor slugify method (use parameterize + create a module)
- [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
NICE TO HAVE:
- import / export site
- asset collections: custom resizing if image
- super_finder
- better icons for mime type
@ -51,7 +42,6 @@ NICE TO HAVE:
- page with regexp url ?
- page redirection (option)
- automatic update !
- import / export site
- page not found (front) => if logged in, link to create the page
DONE:
@ -108,3 +98,41 @@ x stylish file field
x remove not used editable element all in once
x default content from parent editable element
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 'locomotive/patches'
require 'locomotive/version'
require 'locomotive/core_ext'
require 'locomotive/configuration'
require 'locomotive/logger'
require 'locomotive/liquid'
@ -15,6 +14,9 @@ require 'locomotive/admin_responder'
require 'locomotive/routing'
require 'locomotive/regexps'
require 'locomotive/render'
require 'locomotive/import'
require 'locomotive/delayed_job'
require 'locomotive/middlewares'
require 'locomotive/session_store'

View File

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

View File

@ -39,17 +39,21 @@ module CarrierWave
module Mongoid
def validates_integrity_of(*attrs)
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|
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
def validates_processing_of(*attrs)
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|
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

View File

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

View File

@ -5,7 +5,8 @@ module CustomFields
class FileUploader < ::CarrierWave::Uploader::Base
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
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|
Dir[File.join(File.dirname(__FILE__), 'liquid', dir, '*.rb')].each { |lib| require lib }
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
def initialize(site)
@site = site
def before_method(meth)
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
def before_method(meth)
@site.asset_collections.where(:slug => meth.to_s)
return '' if @collection.nil?
@collection.send(meth)
end
end

View File

@ -3,21 +3,16 @@ module Locomotive
module Drops
class Contents < ::Liquid::Drop
def initialize(site)
@site = site
end
def before_method(meth)
type = @site.content_types.where(:slug => meth.to_s).first
ProxyCollection.new(@site, type)
type = @context.registers[:site].content_types.where(:slug => meth.to_s).first
ProxyCollection.new(type)
end
end
class ProxyCollection < ::Liquid::Drop
def initialize(site, content_type)
@site = site
def initialize(content_type)
@content_type = content_type
@collection = nil
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
def stylesheet_tag(input)
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')
%{<link href="#{input}" media="screen" rel="stylesheet" type="text/css" />}
end
@ -15,16 +27,41 @@ module Locomotive
# input: url of the javascript file
def javascript_tag(input)
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')
%{<script src="#{input}" type="text/javascript"></script>}
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
# input: url of the image OR asset drop
def image_tag(input, *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
# Embed a flash movie into a page
@ -32,7 +69,7 @@ module Locomotive
# width: width (in pixel or in %) of the embedded movie
# height: height (in pixel or in %) of the embedded movie
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))
%{
<object #{embed_options}>
@ -100,9 +137,9 @@ module Locomotive
(options.stringify_keys.to_a.collect { |a, b| "#{a}=\"#{b}\"" }).join(' ') << ' '
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
def get_path_from_asset(input)
def get_url_from_asset(input)
input.respond_to?(:url) ? input.url : input
end
end

View File

@ -5,7 +5,7 @@ module Locomotive
#
# 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 %}
# {{ post.title }}
# {% endfor %}
@ -23,6 +23,8 @@ module Locomotive
markup.scan(::Liquid::TagAttributes) do |key, value|
@options[key] = value if key != 'http'
end
@expires_in = (@options.delete('expires_in') || 0).to_i
@cache_key = Digest::SHA1.hexdigest(@target)
else
raise ::Liquid::SyntaxError.new("Syntax Error in 'consume' - Valid syntax: consume <var> from \"<url>\" [username: value, password: value]")
end
@ -31,12 +33,20 @@ module Locomotive
end
def render(context)
render_all_and_cache_it(context)
end
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

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

@ -17,7 +17,7 @@ module Locomotive
nodelist.any? do |node|
if node.is_a?(::Liquid::Variable) && node.name == 'block.super'
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)
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/document'
## various patches
module Mongoid #:nodoc:
module Document
def update_child_with_noname(child, clear = false)
@ -13,9 +13,21 @@ module Mongoid #:nodoc:
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
module Mongoid #:nodoc:
module Validations #:nodoc:
class AssociatedValidator < ActiveModel::EachValidator

View File

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

View File

@ -81,6 +81,9 @@ end
# # empty page (imac 27'): User System Total Real
# 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
# 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
# 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 CodeMirrorEditors = [];
@ -128,7 +127,7 @@ $(document).ready(function() {
$('code.html textarea').each(function() { addCodeMirrorEditor('liquid', $(this)); });
// 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();
} });

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')
$('a#image-picker-link').imagepicker({
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'))
.addClass('asset');
asset.find('h4 a').attr('href', json.url)
.attr('data-slug', json.slug)
.attr('data-shortcut-url', json.shortcut_url)
.html(json.name).bind('click', function(e) {
asset.find('strong a').attr('href', json.url)
.attr('data-local-path', json.local_path)
.html(json.local_path).bind('click', function(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)
asset.addClass('last');
@ -74,7 +74,7 @@ $.fn.imagepicker = function(options) {
'onComplete': function() {
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({
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;
position: relative;
clear: both;
background: transparent url(/images/admin/list/item.png) no-repeat 0 0;
}
ul.list li em {
@ -64,6 +63,13 @@ ul.list li em {
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 {
position: relative;
top: 2px;
@ -71,6 +77,7 @@ ul.list li strong a {
text-decoration: none;
color: #1f82bc;
font-size: 0.9em;
text-shadow: 1px 1px 1px #fff;
}
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#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-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;
}
/* ___ snippets ___ */
/* ___ pages ___ */
@ -176,6 +190,7 @@ div#uploadAssetsInputQueue { display: none; }
color: #1f82bc;
font-size: 0.9em;
padding-left: 6px;
text-shadow: 1px 1px 1px #fff;
}
#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 #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 ___ */
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 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 li.new-asset { display: none; }
div.asset-picker ul { margin: 0px; }
div.asset-picker ul li .more { top: 8px; }
/* ___ custom fields ___ */
#edit-custom-field {

View File

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

View File

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

View File

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

View File

@ -4,20 +4,47 @@ describe Locomotive::Liquid::Filters::Html do
include Locomotive::Liquid::Filters::Html
before(:each) do
@context = build_context
end
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').should == result
stylesheet_tag(nil).should == ''
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
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').should == result
javascript_tag(nil).should == ''
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
image_tag('foo.jpg').should == "<img src=\"/foo.jpg\" />"
end
@ -79,4 +106,15 @@ describe Locomotive::Liquid::Filters::Html do
html.should == ''
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

View File

@ -23,10 +23,28 @@ describe ThemeAsset do
@asset.height.should == 32
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.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
@ -64,10 +82,12 @@ describe ThemeAsset do
before(:each) do
ThemeAsset.any_instance.stubs(:site_id).returns('test')
@asset = Factory.build(:theme_asset, :site => Factory.build(:site))
@asset.performing_plain_text = true
@asset.slug = 'a file'
@asset.plain_text = "Lorem ipsum"
@asset = Factory.build(:theme_asset, {
:site => Factory.build(:site),
:plain_text_name => 'test',
:plain_text => 'Lorem ipsum',
:performing_plain_text => true
})
end
it 'should handle stylesheet' do
@ -84,24 +104,6 @@ describe ThemeAsset do
@asset.source.should_not be_nil
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