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,6 +1,8 @@
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'
@ -26,8 +28,10 @@ module Admin
render :json => { render :json => {
:status => 'success', :status => 'success',
:name => truncate(@theme_asset.slug, :length => 22), :name => truncate(@theme_asset.slug, :length => 22),
:slug => @theme_asset.slug,
:url => @theme_asset.source.url, :url => @theme_asset.source.url,
:vignette_url => @theme_asset.vignette_url :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' } }

View File

@ -11,6 +11,7 @@ class ThemeAsset
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 ##
@ -44,16 +45,17 @@ class ThemeAsset
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?
@ -65,12 +67,32 @@ class ThemeAsset
def store_plain_text def store_plain_text
return if self.plain_text.blank? return if self.plain_text.blank?
# 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]
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({ self.source = CarrierWave::SanitizedFile.new({
:tempfile => StringIO.new(self.plain_text), :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

View File

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

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

@ -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

@ -194,7 +194,7 @@ 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

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:
@ -65,3 +63,5 @@ x publish event when saving form in ajax (for instance, in order to update accou
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

@ -52,6 +52,7 @@ 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]
} }

View File

@ -5,10 +5,12 @@ module Locomotive
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
if self.respond_to?(:before_filter)
before_filter :fetch_site before_filter :fetch_site
helper_method :current_site helper_method :current_site
end end
end
module InstanceMethods module InstanceMethods

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,10 +1,17 @@
$(document).ready(function() { $.fn.imagepicker = function(options) {
var defaults = {
insertFn: null
};
var options = $.extend(defaults, options);
var copyLinkToEditor = function(link, event) { var copyLinkToEditor = function(link, event) {
var editor = CodeMirrorEditors[0].editor; var editor = CodeMirrorEditors[0].editor;
var handle = editor.cursorLine(), position = editor.cursorPosition(handle).character; var handle = editor.cursorLine(), position = editor.cursorPosition(handle).character;
editor.insertIntoLine(handle, position, link.attr('href')); var value = options.insertFn != null ? options.insertFn(link) : link.attr('href');
editor.insertIntoLine(handle, position, value);
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
@ -40,7 +47,10 @@ $(document).ready(function() {
.insertBefore($('.asset-picker ul li.clear')) .insertBefore($('.asset-picker ul li.clear'))
.addClass('asset'); .addClass('asset');
asset.find('h4 a').attr('href', json.url).html(json.name).bind('click', function(e) { 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); copyLinkToEditor($(this), e);
}); });
asset.find('.image .inside img').attr('src', json.vignette_url); asset.find('.image .inside img').attr('src', json.vignette_url);
@ -59,7 +69,8 @@ $(document).ready(function() {
uploader.init(); uploader.init();
} }
$('a#image-picker-link').fancybox({ return this.each(function() {
$(this).fancybox({
'onComplete': function() { 'onComplete': function() {
setupUploader(); setupUploader();
@ -67,3 +78,4 @@ $(document).ready(function() {
} }
}); });
}); });
};

View File

@ -13,4 +13,10 @@ $(document).ready(function() {
}); });
$('#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

@ -35,4 +35,9 @@ $(document).ready(function() {
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

@ -117,11 +117,11 @@ 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;
@ -149,7 +149,7 @@ ul.assets li.asset div.image div.inside {
ul.assets li.asset div.actions { ul.assets li.asset div.actions {
position: absolute; position: absolute;
top: 7px; top: 4px;
right: 12px; right: 12px;
} }

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