allow users to use shortcut for theme images when editing snippets / layouts / stylesheets + fix minor ui bugs

This commit is contained in:
dinedine 2010-07-23 00:10:40 +02:00
parent a03b631a71
commit 2608430cae
21 changed files with 461 additions and 359 deletions

View File

@ -19,7 +19,7 @@ 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 (beta 3) * Rails 3 (beta 4)
* Mongoid * Mongoid
* Liquid * Liquid
* Devise * Devise
@ -30,12 +30,17 @@ h2. Installation
See the "official website":http://www.locomotiveapp.org See the "official website":http://www.locomotiveapp.org
h2. Team
* Developers: "Didier Lafforgue":http://www.nocoffee.fr, "Jacques Crocker":http://www.railsjedi.com
* UI Designer: "Sacha Greif":http://www.sachagreif.com
h2. Credits h2. Credits
Many thanks to "Sacha Greif":http://www.sachagreif.com for his great work on the user interface and the LocomotiveApp website front page.
"Rodrigo Alvarez":http://blog.codecaster.es/ for his plugin named Congo which gave us a good starting point and for his availability for (very late) tech discussions. "Rodrigo Alvarez":http://blog.codecaster.es/ for his plugin named Congo which gave us a good starting point and for his availability for (very late) tech discussions.
"Emmanuel Grard":http://www.grardesign.com designed the awesome locomotive illustration in the LocomotiveApp.org landing page.
h2. Contact h2. Contact
Feel free to contact me at didier at nocoffee dot fr. Feel free to contact me at didier at nocoffee dot fr.

View File

@ -1,39 +1,43 @@
module Admin module Admin
class ThemeAssetsController < BaseController class ThemeAssetsController < BaseController
include ActionView::Helpers::SanitizeHelper
extend ActionView::Helpers::SanitizeHelper::ClassMethods
include ActionView::Helpers::TextHelper include ActionView::Helpers::TextHelper
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.all
@non_image_assets = assets.find_all { |a| a.stylesheet? || a.javascript? } @non_image_assets = assets.find_all { |a| a.stylesheet? || a.javascript? }
@image_assets = assets.find_all { |a| a.image? } @image_assets = assets.find_all { |a| a.image? }
@flash_assets = assets.find_all { |a| a.movie? } @flash_assets = assets.find_all { |a| a.movie? }
if request.xhr? if request.xhr?
render :action => 'images', :layout => false and return render :action => 'images', :layout => false and return
end end
end end
def create def create
params[:theme_asset] = { :source => params[:file] } if params[:file] params[:theme_asset] = { :source => params[:file] } if params[:file]
create! do |success, failure| create! do |success, failure|
success.json do success.json do
render :json => { render :json => {
:status => 'success', :status => 'success',
:name => truncate(@theme_asset.slug, :length => 22), :name => truncate(@theme_asset.slug, :length => 22),
:url => @theme_asset.source.url, :slug => @theme_asset.slug,
:vignette_url => @theme_asset.vignette_url :url => @theme_asset.source.url,
:vignette_url => @theme_asset.vignette_url,
:shortcut_url => @theme_asset.shortcut_url
} }
end end
failure.json { render :json => { :status => 'error' } } failure.json { render :json => { :status => 'error' } }
end end
end end
end end
end end

View File

@ -1,96 +1,118 @@
class ThemeAsset class ThemeAsset
include Locomotive::Mongoid::Document include Locomotive::Mongoid::Document
## Extensions ## ## Extensions ##
include Models::Extensions::Asset::Vignette include Models::Extensions::Asset::Vignette
## fields ## ## fields ##
field :slug field :slug
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
mount_uploader :source, ThemeAssetUploader mount_uploader :source, ThemeAssetUploader
## associations ## ## associations ##
belongs_to_related :site belongs_to_related :site
## callbacks ## ## callbacks ##
before_validation :sanitize_slug before_validation :sanitize_slug
before_validation :store_plain_text before_validation :store_plain_text
before_save :set_slug before_save :set_slug
## validations ## ## validations ##
validate :extname_can_not_be_changed 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 :slug, :if => Proc.new { |a| a.new_record? && a.performing_plain_text? }
validates_uniqueness_of :slug, :scope => [:site_id, :content_type] validates_uniqueness_of :slug, :scope => [:site_id, :content_type]
validates_integrity_of :source validates_integrity_of :source
## accessors ## ## accessors ##
attr_accessor :performing_plain_text attr_accessor :performing_plain_text
## methods ## ## methods ##
%w{movie image stylesheet javascript font}.each do |type| %w{movie image stylesheet javascript font}.each do |type|
define_method("#{type}?") do define_method("#{type}?") do
self.content_type == type self.content_type == type
end end
end end
def stylesheet_or_javascript? def stylesheet_or_javascript?
self.stylesheet? || self.javascript? self.stylesheet? || self.javascript?
end end
def plain_text def plain_text
@plain_text ||= (if self.stylesheet_or_javascript? if self.stylesheet_or_javascript?
self.source.read self.plain_text = self.source.read if read_attribute(:plain_text).blank?
read_attribute(:plain_text)
else else
nil nil
end) end
end end
def plain_text=(source) def plain_text=(source)
self.performing_plain_text = true if self.performing_plain_text.nil? self.performing_plain_text = true if self.performing_plain_text.nil?
@plain_text = source write_attribute(:plain_text, source)
end end
def performing_plain_text? def performing_plain_text?
return true if !self.new_record? && self.stylesheet_or_javascript? && self.errors.empty? 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) !(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? return if self.plain_text.blank?
self.source = CarrierWave::SanitizedFile.new({ # replace /theme/<content_type>/<slug> occurences by the real amazon S3 url or local files
:tempfile => StringIO.new(self.plain_text), 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
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({
:tempfile => StringIO.new(sanitized_source),
:filename => "#{self.slug}.#{self.stylesheet? ? 'css' : 'js'}" :filename => "#{self.slug}.#{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 sanitize_slug
self.slug.slugify!(:underscore => true) if self.slug.present? self.slug.slugify!(:underscore => true) if self.slug.present?
end end
def set_slug def set_slug
if self.slug.blank? if self.slug.blank?
self.slug = File.basename(self.source_filename, File.extname(self.source_filename)) self.slug = File.basename(self.source_filename, File.extname(self.source_filename))
self.sanitize_slug self.sanitize_slug
end end
end end
def extname_can_not_be_changed def extname_can_not_be_changed
return if self.new_record? return if self.new_record?
if File.extname(self.source.file.original_filename) != File.extname(self.source_filename) if File.extname(self.source.file.original_filename) != File.extname(self.source_filename)
self.errors.add(:source, :extname_changed) self.errors.add(:source, :extname_changed)
end end

View File

@ -1,10 +1,10 @@
- content_for :head do - content_for :head do
= javascript_include_tag 'admin/plugins/codemirror/codemirror' = javascript_include_tag 'admin/plugins/codemirror/codemirror', 'admin/layouts.js'
= image_picker_include_tags = image_picker_include_tags
= f.inputs :name => :information do = f.inputs :name => :information do
= f.input :name = f.input :name
= f.inputs :name => :code do = f.inputs :name => :code do
= f.custom_input :value, :css => 'code full', :with_label => false do = f.custom_input :value, :css => 'code full', :with_label => false do
%code{ :class => 'html' } %code{ :class => 'html' }

View File

@ -3,7 +3,7 @@
- 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'} #{'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 %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 .image
.inside .inside
= vignette_tag(asset) = vignette_tag(asset)

View File

@ -7,6 +7,7 @@
#file-selector{ :class => "selector #{'hidden' if @theme_asset.performing_plain_text?}" } #file-selector{ :class => "selector #{'hidden' if @theme_asset.performing_plain_text?}" }
= 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

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) %p!= t('.help', :url => @theme_asset.source.url, :shortcut_url => @theme_asset.shortcut_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

@ -16,9 +16,9 @@ Locomotive::Application.configure do
# Don't care if the mailer can't send # Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = false
# config.action_mailer.default_url_options = { :host => 'localhost:3000' } # config.action_mailer.default_url_options = { :host => 'localhost:3000' }
# MockSmtp settings # MockSmtp settings
config.action_mailer.delivery_method = :smtp config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { config.action_mailer.smtp_settings = {

View File

@ -172,7 +172,7 @@ en:
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 use it by copying/pasting the following url: %{url}" help: "You can insert the following shortcut url in your stylesheets: <strong>%{shortcut_url}</strong> OR use the direct 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

View File

@ -14,14 +14,14 @@ fr:
edit: edit:
title: Changer mon mot de passe title: Changer mon mot de passe
link: "&rarr; Retour page de connexion" link: "&rarr; Retour page de connexion"
buttons: buttons:
login: Se connecter login: Se connecter
send_password: Envoyer send_password: Envoyer
change_password: Changer change_password: Changer
fr: fr:
admin: admin:
buttons: buttons:
login: Se connecter login: Se connecter
send_password: Envoyer send_password: Envoyer
@ -134,7 +134,7 @@ fr:
new: nouveau gabarit new: nouveau gabarit
layout: layout:
updated_at: Mis à jour le updated_at: Mis à jour le
snippets: snippets:
index: index:
title: Liste des snippets title: Liste des snippets
@ -149,28 +149,28 @@ fr:
help: "Remplissez le formulaire ci-dessous pour mettre à jour votre snippet." help: "Remplissez le formulaire ci-dessous pour mettre à jour votre snippet."
snippet: snippet:
updated_at: Mis à jour le updated_at: Mis à jour le
sites: sites:
new: new:
title: "Nouveau site" title: "Nouveau site"
help: "Remplissez le formulaire ci-dessous pour créer votre nouveau site." help: "Remplissez le formulaire ci-dessous pour créer votre nouveau site."
current_sites: current_sites:
edit: edit:
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"
memberships: memberships:
new: new:
title: "Ajout d'un compte" title: "Ajout d'un compte"
help: "Donnez l'adresse email du compte à ajouter. S'il n'existe pas, vous serez redirigé(e) vers le formulaire de création d'un compte." help: "Donnez l'adresse email du compte à ajouter. S'il n'existe pas, vous serez redirigé(e) vers le formulaire de création d'un compte."
accounts: accounts:
new: new:
title: Nouveau compte title: Nouveau compte
help: "Remplissez le formulaire ci-dessous pour ajouter un nouveau compte." help: "Remplissez le formulaire ci-dessous pour ajouter un nouveau compte."
my_accounts: my_accounts:
edit: edit:
help: "Votre nom est modifiable en cliquant dessus." help: "Votre nom est modifiable en cliquant dessus."
@ -178,7 +178,7 @@ fr:
en: en Anglais en: en Anglais
fr: en Français fr: en Français
ask_for_name: "Veuillez entrer le nouveau nom" ask_for_name: "Veuillez entrer le nouveau nom"
theme_assets: theme_assets:
index: index:
title: Liste des fichiers du thème title: Liste des fichiers du thème
@ -194,14 +194,14 @@ fr:
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."
edit: edit:
title: "Edition %{file}" title: "Edition %{file}"
help: "Vous pouvez utiliser ce fichier grâce a l'url suivante: %{url}" help: "Vous pouvez insérer le raccourci suivant dans vos feuilles de style: <strong>%{shortcut_url}</strong> OU utiliser directement l'url : %{url}"
form: form:
choose_file: Choisir fichier choose_file: Choisir fichier
choose_plain_text: Passer en mode texte choose_plain_text: Passer en mode texte
images: images:
title: Liste des images title: Liste des images
no_items: "Il n'y a pas d'images." no_items: "Il n'y a pas d'images."
asset_collections: asset_collections:
index: index:
title: Collections title: Collections
@ -217,7 +217,7 @@ fr:
destroy: supprimer collection destroy: supprimer collection
no_items: "Il n'existe pas de médias. Vous pouvez commencer par créer un <a href='%{url}'>ici</a>." no_items: "Il n'existe pas de médias. Vous pouvez commencer par créer un <a href='%{url}'>ici</a>."
ask_for_name: "Veuillez entrer le nouveau nom" ask_for_name: "Veuillez entrer le nouveau nom"
assets: assets:
new: new:
title: "Nouveau média" title: "Nouveau média"
@ -225,7 +225,7 @@ fr:
edit: edit:
title: "Edition média" title: "Edition média"
help: "Remplissez le formulaire ci-dessous pour mettre à jour votre média." help: "Remplissez le formulaire ci-dessous pour mettre à jour votre média."
content_types: content_types:
index: index:
new: nouveau modèle new: nouveau modèle
@ -241,27 +241,27 @@ fr:
order_by: order_by:
updated_at: 'Par date de mise à jour' updated_at: 'Par date de mise à jour'
position_in_list: Manuellement position_in_list: Manuellement
contents: contents:
index: index:
title: 'Liste des "%{type}"' title: 'Liste des "%{type}"'
edit: éditer modèle edit: éditer modèle
destroy: supprimer modèle destroy: supprimer modèle
download: télécharger éléments download: télécharger éléments
new: nouvel élément new: nouvel élément
category_noname: "Pas de nom" category_noname: "Pas de nom"
lastest_items: "Eléments récents" lastest_items: "Eléments récents"
updated_at: "Mis à jour le" updated_at: "Mis à jour le"
list: list:
no_items: "Il n'existe pas d'éléments. Vous pouvez commencer par créer un <a href='%{url}'>ici</a>" no_items: "Il n'existe pas d'éléments. Vous pouvez commencer par créer un <a href='%{url}'>ici</a>"
new: new:
title: '%{type} &mdash; nouvel élément' title: '%{type} &mdash; nouvel élément'
edit: edit:
title: '%{type} &mdash; édition élément' title: '%{type} &mdash; édition élément'
image_picker: image_picker:
link: Insérer une image dans le code link: Insérer une image dans le code
formtastic: formtastic:
titles: titles:
information: Informations générales information: Informations générales
@ -289,7 +289,7 @@ fr:
custom_fields: custom_fields:
field: field:
_alias: Alias _alias: Alias
hints: hints:
page: page:
published: "Seuls les administrateurs authentifiés peuvent voir une page non publiée." published: "Seuls les administrateurs authentifiés peuvent voir une page non publiée."

View File

@ -1,57 +1,57 @@
# Locomotive::Application.routes.draw do |map| # Locomotive::Application.routes.draw do |map|
Rails.application.routes.draw do |map| Rails.application.routes.draw do |map|
constraints(Locomotive::Routing::DefaultConstraint) do constraints(Locomotive::Routing::DefaultConstraint) do
root :to => 'home#show' root :to => 'home#show'
end end
# admin authentication # admin authentication
devise_for :admin, :class_name => 'Account', :controllers => { :sessions => 'admin/sessions', :passwords => 'admin/passwords' } devise_for :admin, :class_name => 'Account', :controllers => { :sessions => 'admin/sessions', :passwords => 'admin/passwords' }
# admin interface for each website # admin interface for each website
namespace 'admin' do namespace 'admin' do
root :to => 'pages#index' root :to => 'pages#index'
resources :pages do resources :pages do
put :sort, :on => :member put :sort, :on => :member
get :get_path, :on => :collection get :get_path, :on => :collection
end end
resources :layouts do resources :layouts do
resources :page_parts, :only => :index resources :page_parts, :only => :index
end end
resources :snippets resources :snippets
resources :site resources :site
resource :current_site resource :current_site
resources :accounts resources :accounts
resource :my_account resource :my_account
resources :memberships resources :memberships
resources :theme_assets resources :theme_assets
resources :asset_collections resources :asset_collections
resources :assets, :path => 'asset_collections/:collection_id/assets' resources :assets, :path => 'asset_collections/:collection_id/assets'
resources :content_types resources :content_types
resources :contents, :path => 'content_types/:slug/contents' do resources :contents, :path => 'content_types/:slug/contents' do
put :sort, :on => :collection put :sort, :on => :collection
end end
resources :api_contents, :path => 'api/:slug/contents', :controller => 'api_contents', :only => [:create] resources :api_contents, :path => 'api/:slug/contents', :controller => 'api_contents', :only => [:create]
resources :custom_fields, :path => 'custom/:parent/:slug/fields' resources :custom_fields, :path => 'custom/:parent/:slug/fields'
end end
# sitemap # sitemap
match '/sitemap.xml' => 'admin/sitemaps#show', :format => 'xml' match '/sitemap.xml' => 'admin/sitemaps#show', :format => 'xml'
# magic urls # magic urls
match '/' => 'admin/rendering#show' match '/' => 'admin/rendering#show'
match '*path' => 'admin/rendering#show' match '*path' => 'admin/rendering#show'

View File

@ -1,13 +1,10 @@
BOARD: BOARD:
- refactor slugify method (use parameterize + create a module) - refactor slugify method (use parameterize + create a module)
- send email when new content added thru api
BACKLOG: BACKLOG:
- rack app to map pretty asset url to S3
- notify accounts when new instance of models (opt): none, one or many accounts. Used for contact form. - notify accounts when new instance of models (opt): none, one or many accounts. Used for contact form.
- theme assets: disable version if not image
- new custom field types: - new custom field types:
- belongs_to => association - belongs_to => association
- cucumber features for admin pages - cucumber features for admin pages
@ -28,6 +25,7 @@ NICE TO HAVE:
- page with regexp url ? - page with regexp url ?
- page redirection (option) - page redirection (option)
- automatic update ! - automatic update !
- import / export site
DONE: DONE:
@ -64,4 +62,6 @@ x change action icons according to the right action [Sacha]
x publish event when saving form in ajax (for instance, in order to update account name or site name) x publish event when saving form in ajax (for instance, in order to update account name or site name)
x page templatized (bound to a model) x page templatized (bound to a model)
x theme asset picker when editing layout / snippet x theme asset picker when editing layout / snippet
x templatized: do not display content with visible / active set to false x templatized: do not display content with visible / active set to false
x theme assets: disable version if not image (handled by the new version of Carrierwave)
x rack app to map pretty asset url to S3 => shortcut urls instead

View File

@ -0,0 +1,19 @@
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

@ -1,40 +1,40 @@
module Locomotive module Locomotive
module Render module Render
extend ActiveSupport::Concern extend ActiveSupport::Concern
module InstanceMethods module InstanceMethods
protected protected
def render_locomotive_page def render_locomotive_page
@page = locomotive_page @page = locomotive_page
redirect_to application_root_url and return if @page.nil? redirect_to application_root_url and return if @page.nil?
output = @page.render(locomotive_context) output = @page.render(locomotive_context)
prepare_and_set_response(output) prepare_and_set_response(output)
end end
def locomotive_page def locomotive_page
path = request.fullpath.clone path = request.fullpath.clone
path.gsub!(/\.[a-zA-Z][a-zA-Z0-9]{2,}$/, '') path.gsub!(/\.[a-zA-Z][a-zA-Z0-9]{2,}$/, '')
path.gsub!(/^\//, '') path.gsub!(/^\//, '')
path = 'index' if path.blank? path = 'index' if path.blank?
if path != 'index' if path != 'index'
dirname = File.dirname(path).gsub(/^\.$/, '') # also look for templatized page path dirname = File.dirname(path).gsub(/^\.$/, '') # also look for templatized page path
path = [path, File.join(dirname, 'content_type_template').gsub(/^\//, '')] path = [path, File.join(dirname, 'content_type_template').gsub(/^\//, '')]
end end
if page = current_site.pages.any_in(:fullpath => [*path]).first if page = current_site.pages.any_in(:fullpath => [*path]).first
if not page.published? and current_admin.nil? if not page.published? and current_admin.nil?
page = nil page = nil
else else
if page.templatized? if page.templatized?
@content_instance = page.content_type.contents.where(:_slug => File.basename(path.first)).first @content_instance = page.content_type.contents.where(:_slug => File.basename(path.first)).first
if @content_instance.nil? || (!@content_instance.visible? && current_admin.nil?) # content instance not found or not visible if @content_instance.nil? || (!@content_instance.visible? && current_admin.nil?) # content instance not found or not visible
page = nil page = nil
end end
@ -44,7 +44,7 @@ module Locomotive
page || current_site.pages.not_found.published.first page || current_site.pages.not_found.published.first
end end
def locomotive_context def locomotive_context
assigns = { assigns = {
'site' => current_site, 'site' => current_site,
@ -52,35 +52,36 @@ module Locomotive
'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new(current_site), 'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new(current_site),
'stylesheets' => Locomotive::Liquid::Drops::Stylesheets.new(current_site), 'stylesheets' => Locomotive::Liquid::Drops::Stylesheets.new(current_site),
'javascripts' => Locomotive::Liquid::Drops::Javascripts.new(current_site), 'javascripts' => Locomotive::Liquid::Drops::Javascripts.new(current_site),
'theme_images' => Locomotive::Liquid::Drops::ThemeImages.new(current_site),
'contents' => Locomotive::Liquid::Drops::Contents.new(current_site), 'contents' => Locomotive::Liquid::Drops::Contents.new(current_site),
'current_page' => self.params[:page] 'current_page' => self.params[:page]
} }
if @page.templatized? # add instance from content type if @page.templatized? # add instance from content type
assigns['content_instance'] = @content_instance assigns['content_instance'] = @content_instance
assigns[@page.content_type.slug.singularize] = @content_instance # just here to help to write readable liquid code assigns[@page.content_type.slug.singularize] = @content_instance # just here to help to write readable liquid code
end end
registers = { :controller => self, :site => current_site, :page => @page } registers = { :controller => self, :site => current_site, :page => @page }
::Liquid::Context.new(assigns, registers) ::Liquid::Context.new(assigns, registers)
end end
def prepare_and_set_response(output) def prepare_and_set_response(output)
response.headers['Content-Type'] = 'text/html; charset=utf-8' response.headers['Content-Type'] = 'text/html; charset=utf-8'
if @page.with_cache? if @page.with_cache?
fresh_when :etag => @page, :last_modified => @page.updated_at.utc, :public => true fresh_when :etag => @page, :last_modified => @page.updated_at.utc, :public => true
if @page.cache_strategy != 'simple' # varnish if @page.cache_strategy != 'simple' # varnish
response.cache_control[:max_age] = @page.cache_strategy response.cache_control[:max_age] = @page.cache_strategy
end end
end end
render :text => output, :layout => false, :status => :ok render :text => output, :layout => false, :status => :ok
end end
end end
end end
end end

View File

@ -1,43 +1,45 @@
module Locomotive module Locomotive
module Routing module Routing
module SiteDispatcher module SiteDispatcher
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
before_filter :fetch_site if self.respond_to?(:before_filter)
before_filter :fetch_site
helper_method :current_site
helper_method :current_site
end
end end
module InstanceMethods module InstanceMethods
protected protected
def fetch_site def fetch_site
Locomotive.logger "[fetch site] host = #{request.host} / #{request.env['HTTP_HOST']}" Locomotive.logger "[fetch site] host = #{request.host} / #{request.env['HTTP_HOST']}"
@current_site ||= Site.match_domain(request.host).first @current_site ||= Site.match_domain(request.host).first
end end
def current_site def current_site
@current_site || fetch_site @current_site || fetch_site
end end
def require_site def require_site
redirect_to application_root_url and return false if current_site.nil? redirect_to application_root_url and return false if current_site.nil?
end end
def validate_site_membership def validate_site_membership
return if current_site && current_site.accounts.include?(current_admin) return if current_site && current_site.accounts.include?(current_admin)
redirect_to application_root_url redirect_to application_root_url
end end
def application_root_url def application_root_url
root_url(:host => Locomotive.config.default_domain, :port => request.port) root_url(:host => Locomotive.config.default_domain, :port => request.port)
end end
end end
end end
end end
end end

View File

@ -0,0 +1,7 @@
$(document).ready(function() {
$('a#image-picker-link').imagepicker({
insertFn: function(link) {
return "{{ theme_images." + link.attr('data-slug') + " }}";
}
});
});

View File

@ -1,69 +1,81 @@
$(document).ready(function() { $.fn.imagepicker = function(options) {
var copyLinkToEditor = function(link, event) {
var editor = CodeMirrorEditors[0].editor;
var handle = editor.cursorLine(), position = editor.cursorPosition(handle).character;
editor.insertIntoLine(handle, position, link.attr('href')); var defaults = {
insertFn: null
};
var options = $.extend(defaults, options);
event.stopPropagation(); var copyLinkToEditor = function(link, event) {
event.preventDefault(); var editor = CodeMirrorEditors[0].editor;
var handle = editor.cursorLine(), position = editor.cursorPosition(handle).character;
$.fancybox.close(); var value = options.insertFn != null ? options.insertFn(link) : link.attr('href');
}
var setupUploader = function() { editor.insertIntoLine(handle, position, value);
var multipartParams = {};
multipartParams[$('meta[name=csrf-param]').attr('content')] = $('meta[name=csrf-token]').attr('content');
var uploader = new plupload.Uploader({ event.stopPropagation();
runtimes : (jQuery.browser.webkit == true ? 'flash' : 'html5,flash'), event.preventDefault();
container: 'theme-images',
browse_button : 'upload-link',
max_file_size : '5mb',
url : $('a#upload-link').attr('href'),
flash_swf_url : '/javascripts/admin/plugins/plupload/plupload.flash.swf',
multipart: true,
multipart_params: multipartParams
});
uploader.bind('QueueChanged', function() { $.fancybox.close();
uploader.start(); }
});
uploader.bind('FileUploaded', function(up, file, response) { var setupUploader = function() {
var json = JSON.parse(response.response); var multipartParams = {};
multipartParams[$('meta[name=csrf-param]').attr('content')] = $('meta[name=csrf-token]').attr('content');
if (json.status == 'success') { var uploader = new plupload.Uploader({
var asset = $('.asset-picker ul li.new-asset') runtimes : (jQuery.browser.webkit == true ? 'flash' : 'html5,flash'),
.clone() container: 'theme-images',
.insertBefore($('.asset-picker ul li.clear')) browse_button : 'upload-link',
.addClass('asset'); max_file_size : '5mb',
url : $('a#upload-link').attr('href'),
flash_swf_url : '/javascripts/admin/plugins/plupload/plupload.flash.swf',
multipart: true,
multipart_params: multipartParams
});
asset.find('h4 a').attr('href', json.url).html(json.name).bind('click', function(e) { uploader.bind('QueueChanged', function() {
copyLinkToEditor($(this), e); uploader.start();
}); });
asset.find('.image .inside img').attr('src', json.vignette_url);
if ($('.asset-picker ul li.asset').length % 3 == 0) uploader.bind('FileUploaded', function(up, file, response) {
asset.addClass('last'); var json = JSON.parse(response.response);
asset.removeClass('new-asset'); if (json.status == 'success') {
var asset = $('.asset-picker ul li.new-asset')
.clone()
.insertBefore($('.asset-picker ul li.clear'))
.addClass('asset');
$('.asset-picker p.no-items').hide(); 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) {
copyLinkToEditor($(this), e);
});
asset.find('.image .inside img').attr('src', json.vignette_url);
$('.asset-picker ul').scrollTo($('li.asset:last'), 400); if ($('.asset-picker ul li.asset').length % 3 == 0)
} asset.addClass('last');
});
uploader.init(); asset.removeClass('new-asset');
}
$('a#image-picker-link').fancybox({ $('.asset-picker p.no-items').hide();
'onComplete': function() {
setupUploader(); $('.asset-picker ul').scrollTo($('li.asset:last'), 400);
}
$('ul.assets h4 a').bind('click', function(e) { copyLinkToEditor($(this), e); }); });
}
}); uploader.init();
}); }
return this.each(function() {
$(this).fancybox({
'onComplete': function() {
setupUploader();
$('ul.assets h4 a').bind('click', function(e) { copyLinkToEditor($(this), e); });
}
});
});
};

View File

@ -1,16 +1,22 @@
$(document).ready(function() { $(document).ready(function() {
// automatic slug from snippet name // automatic slug from snippet name
$('#snippet_name').keypress(function() { $('#snippet_name').keypress(function() {
var input = $(this); var input = $(this);
var slug = $('#snippet_slug'); var slug = $('#snippet_slug');
if (!slug.hasClass('filled')) { if (!slug.hasClass('filled')) {
setTimeout(function() { setTimeout(function() {
slug.val(makeSlug(input.val())); slug.val(makeSlug(input.val()));
}, 50); }, 50);
} }
}); });
$('#snippet_slug').keypress(function() { $(this).addClass('filled'); }); $('#snippet_slug').keypress(function() { $(this).addClass('filled'); });
$('a#image-picker-link').imagepicker({
insertFn: function(link) {
return "{{ theme_images." + link.attr('data-slug') + " }}";
}
});
}); });

View File

@ -1,38 +1,43 @@
/* ___ file or text ___ */ /* ___ file or text ___ */
var enableFileOrTextToggling = function() { var enableFileOrTextToggling = function() {
$('div.hidden').hide(); $('div.hidden').hide();
$('span.alt').click(function(event) {
event.preventDefault();
if ($("div#file-selector").is(":hidden")) { $('span.alt').click(function(event) {
$("div#text-selector").slideUp("normal", function() { event.preventDefault();
$("div#file-selector").slideDown();
$("input#theme_asset_performing_plain_text").val(false); if ($("div#file-selector").is(":hidden")) {
}); $("div#text-selector").slideUp("normal", function() {
$("div#file-selector").slideDown();
$("input#theme_asset_performing_plain_text").val(false);
});
} else { } else {
$("div#file-selector").slideUp("normal", function() { $("div#file-selector").slideUp("normal", function() {
$("div#text-selector").slideDown(); $("div#text-selector").slideDown();
$("input#theme_asset_performing_plain_text").val(true); $("input#theme_asset_performing_plain_text").val(true);
}); });
} }
}); });
} }
$(document).ready(function() { $(document).ready(function() {
enableFileOrTextToggling(); enableFileOrTextToggling();
$('code.stylesheet textarea').each(function() { $('code.stylesheet textarea').each(function() {
addCodeMirrorEditor(null, $(this), ["tokenizejavascript.js", "parsejavascript.js", "parsecss.js"]); addCodeMirrorEditor(null, $(this), ["tokenizejavascript.js", "parsejavascript.js", "parsecss.js"]);
}); });
$('code.javascript textarea').each(function() { $('code.javascript textarea').each(function() {
addCodeMirrorEditor(null, $(this), ["parsecss.js", "tokenizejavascript.js", "parsejavascript.js"]); addCodeMirrorEditor(null, $(this), ["parsecss.js", "tokenizejavascript.js", "parsejavascript.js"]);
}); });
$('select#theme_asset_content_type').bind('change', function() { $('select#theme_asset_content_type').bind('change', function() {
var editor = CodeMirrorEditors[0].editor; var editor = CodeMirrorEditors[0].editor;
editor.setParser($(this).val() == 'stylesheet' ? 'CSSParser' : 'JSParser'); editor.setParser($(this).val() == 'stylesheet' ? 'CSSParser' : 'JSParser');
}); });
$('a#image-picker-link').imagepicker({
insertFn: function(link) {
return link.attr('data-shortcut-url');
}
});
}); });

View File

@ -1,74 +1,74 @@
/* ___ application messages ___ */ /* ___ application messages ___ */
div.notice { div.notice {
background: transparent url(/images/admin/form/growl-notice.png) repeat-x 0 0; background: transparent url(/images/admin/form/growl-notice.png) repeat-x 0 0;
position: relative; position: relative;
width: 100%; width: 100%;
height: 90px; height: 90px;
} }
div.notice.error, div.notice.alert { div.notice.error, div.notice.alert {
background-image: url(/images/admin/form/growl-error.png); background-image: url(/images/admin/form/growl-error.png);
} }
div.notice p { div.notice p {
position: relative; position: relative;
top: 35px; top: 35px;
margin: 0px; margin: 0px;
text-align: center; text-align: center;
font-size: 1.5em; font-size: 1.5em;
text-shadow: 1px 1px 1px #333; text-shadow: 1px 1px 1px #333;
color: #fff; color: #fff;
} }
/* ___ list ___ */ /* ___ list ___ */
p.no-items { p.no-items {
padding: 15px 0px; padding: 15px 0px;
background: transparent url(/images/admin/list/none.png) no-repeat 0 0; background: transparent url(/images/admin/list/none.png) no-repeat 0 0;
text-align: center; text-align: center;
color: #9d8963 !important; color: #9d8963 !important;
font-size: 1.1em !important; font-size: 1.1em !important;
} }
p.no-items a { p.no-items a {
color: #ff2900; color: #ff2900;
text-decoration: none; text-decoration: none;
} }
p.no-items a:hover { p.no-items a:hover {
text-decoration: underline; text-decoration: underline;
} }
ul.list { ul.list {
list-style: none; list-style: none;
margin: 0px 0 20px 0; margin: 0px 0 20px 0;
background: white; background: white;
} }
ul.list li { ul.list li {
height: 31px; height: 31px;
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; background: transparent url(/images/admin/list/item.png) no-repeat 0 0;
} }
ul.list li em { ul.list li em {
display: block; display: block;
float: left; float: left;
background: transparent url(/images/admin/list/item-left.png) no-repeat left 0; background: transparent url(/images/admin/list/item-left.png) no-repeat left 0;
height: 31px; height: 31px;
width: 18px; width: 18px;
} }
ul.list li strong a { ul.list li strong a {
position: relative; position: relative;
top: 2px; top: 2px;
left: 15px; left: 15px;
text-decoration: none; text-decoration: none;
color: #1f82bc; color: #1f82bc;
font-size: 0.9em; font-size: 0.9em;
} }
ul.list.sortable li strong a { left: 10px; } ul.list.sortable li strong a { left: 10px; }
@ -76,81 +76,81 @@ ul.list.sortable li strong a { left: 10px; }
ul.list li strong a:hover { text-decoration: underline; } ul.list li strong a:hover { text-decoration: underline; }
ul.list li div.more { ul.list li div.more {
position: absolute; position: absolute;
top: 3px; top: 3px;
right: 15px; right: 15px;
font-size: 0.7em; font-size: 0.7em;
color: #8b8d9a; color: #8b8d9a;
} }
ul.list li div.more a { ul.list li div.more a {
margin-left: 10px; margin-left: 10px;
position: relative; position: relative;
top: 4px; top: 4px;
} }
ul.list li span.handle { ul.list li span.handle {
position: relative; position: relative;
top: 5px; top: 5px;
margin: 0 0 0 15px; margin: 0 0 0 15px;
cursor: move; cursor: move;
} }
/* ___ assets ___ */ /* ___ assets ___ */
ul.assets { ul.assets {
list-style: none; list-style: none;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
} }
ul.assets li.asset { ul.assets li.asset {
position: relative; position: relative;
float: left; float: left;
width: 139px; width: 139px;
height: 140px; height: 140px;
background: transparent url(/images/admin/list/thumb.png) no-repeat 0 0; background: transparent url(/images/admin/list/thumb.png) no-repeat 0 0;
margin: 0 17px 17px 0; margin: 0 17px 17px 0;
} }
ul.assets li.asset.last { ul.assets li.asset.last {
margin-right: 0px; margin-right: 0px;
} }
ul.assets li.asset h4 { margin: 0px; height: 30px; border-bottom: 1px solid #ccced7; } ul.assets li.asset h4 { margin: 0px; height: 30px; border-bottom: 1px solid #ccced7; position: relative; }
ul.assets li.asset h4 a { ul.assets li.asset h4 a {
position: relative; position: absolute;
top: 6px; top: 7px;
left: 12px; left: 12px;
font-weight: bold; font-weight: bold;
font-size: 0.6em; font-size: 0.6em;
color: #1f82bc; color: #1f82bc;
text-decoration: none; text-decoration: none;
} }
ul.assets li.asset h4 a:hover { text-decoration: underline; } ul.assets li.asset h4 a:hover { text-decoration: underline; }
ul.assets li.asset div.image { ul.assets li.asset div.image {
width: 80px; width: 80px;
height: 80px; height: 80px;
border: 4px solid #fff; border: 4px solid #fff;
margin: 10px 0 0 24px; margin: 10px 0 0 24px;
background: transparent url(/images/admin/list/empty.png) repeat 0 0; background: transparent url(/images/admin/list/empty.png) repeat 0 0;
} }
ul.assets li.asset div.image div.inside { ul.assets li.asset div.image div.inside {
display: table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
width: 80px; width: 80px;
height: 80px; height: 80px;
} }
ul.assets li.asset div.actions { ul.assets li.asset div.actions {
position: absolute; position: absolute;
top: 7px; top: 4px;
right: 12px; right: 12px;
} }
/* ___ asset collections ___ */ /* ___ asset collections ___ */
@ -164,73 +164,73 @@ div#uploadAssetsInputQueue { display: none; }
#contents-list li { background: none; } #contents-list li { background: none; }
#contents-list.sortable li em { #contents-list.sortable li em {
background-position: left -31px; background-position: left -31px;
cursor: move; cursor: move;
} }
#contents-list li strong { #contents-list li strong {
margin-left: 18px; margin-left: 18px;
display: block; display: block;
height: 31px; height: 31px;
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;
} }
/* ___ pages ___ */ /* ___ pages ___ */
#pages-list { #pages-list {
list-style: none; list-style: none;
margin: 0px 0 20px 0; margin: 0px 0 20px 0;
background: white; background: white;
} }
#pages-list ul { list-style: none; margin: 10px 0 10px 40px; padding: 0; } #pages-list ul { list-style: none; margin: 10px 0 10px 40px; padding: 0; }
#pages-list li { #pages-list li {
margin-bottom: 10px; margin-bottom: 10px;
position: relative; position: relative;
clear: both; clear: both;
} }
#pages-list li em { #pages-list li em {
display: block; display: block;
float: left; float: left;
background: transparent url(/images/admin/list/item-left.png) no-repeat left 0; background: transparent url(/images/admin/list/item-left.png) no-repeat left 0;
height: 31px; height: 31px;
width: 18px; width: 18px;
} }
#pages-list ul.folder li em { #pages-list ul.folder li em {
background-position: left -31px; background-position: left -31px;
cursor: move; cursor: move;
} }
#pages-list ul.folder li.templatized em { #pages-list ul.folder li.templatized em {
background-position: left -62px; background-position: left -62px;
cursor: pointer; cursor: pointer;
} }
#pages-list li .toggler { #pages-list li .toggler {
position: absolute; position: absolute;
top: 9px; top: 9px;
left: -15px; left: -15px;
cursor: pointer; cursor: pointer;
} }
#pages-list li strong { #pages-list li strong {
margin-left: 18px; margin-left: 18px;
display: block; display: block;
height: 31px; height: 31px;
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;
} }
#pages-list li strong a { #pages-list li strong a {
position: relative; position: relative;
top: 3px; top: 3px;
text-decoration: none; text-decoration: none;
color: #1f82bc; color: #1f82bc;
font-size: 0.9em; font-size: 0.9em;
padding-left: 6px; padding-left: 6px;
} }
#pages-list li strong a:hover { text-decoration: underline; } #pages-list li strong a:hover { text-decoration: underline; }
@ -238,18 +238,18 @@ div#uploadAssetsInputQueue { display: none; }
#pages-list li.hidden strong a { font-style: italic; font-weight: normal; } #pages-list li.hidden strong a { font-style: italic; font-weight: normal; }
#pages-list li .more { #pages-list li .more {
position: absolute; position: absolute;
top: 6px; top: 6px;
right: 20px; right: 20px;
font-size: 0.7em; font-size: 0.7em;
color: #8b8d9a; color: #8b8d9a;
} }
#pages-list li .more a { #pages-list li .more a {
position: relative; position: relative;
top: 3px; top: 3px;
margin-left: 10px; margin-left: 10px;
outline: none; outline: none;
} }
#pages-list li.not-found { border-top: 1px dotted #bbbbbd; padding-top: 10px; margin-left: 0px; } #pages-list li.not-found { border-top: 1px dotted #bbbbbd; padding-top: 10px; margin-left: 0px; }

View File

@ -64,7 +64,7 @@ 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) @asset = Factory.build(:theme_asset, :site => Factory.build(:site))
@asset.performing_plain_text = true @asset.performing_plain_text = true
@asset.slug = 'a file' @asset.slug = 'a file'
@asset.plain_text = "Lorem ipsum" @asset.plain_text = "Lorem ipsum"
@ -84,6 +84,24 @@ 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