Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Andrea Frigido 2011-05-23 09:07:35 +02:00
commit 35d5e7d44e
40 changed files with 687 additions and 288 deletions

View File

@ -2,12 +2,12 @@ source :rubygems
# add in all the runtime dependencies
gem 'rails', '>= 3.0.5'
gem 'rails', '>= 3.0.7'
gem 'warden'
gem 'devise', '= 1.1.3'
gem 'mongoid', '2.0.0.rc.7'
gem 'mongoid', '~> 2.0.1'
gem 'bson_ext', '~> 1.3.0'
gem 'locomotive_mongoid_acts_as_tree', '0.1.5.5', :require => 'mongoid_acts_as_tree'
gem 'will_paginate'
@ -20,7 +20,7 @@ gem 'inherited_resources', '~> 1.1.2'
gem 'rmagick', '2.12.2'
gem 'locomotive_carrierwave', '0.5.0.1.beta3', :require => 'carrierwave'
gem 'custom_fields', '1.0.0.beta.10'
gem 'custom_fields', '1.0.0.beta.12'
gem 'fog', '0.3.7'
gem 'mimetype-fu'
gem 'actionmailer-with-request', :require => 'actionmailer_with_request'

View File

@ -1,6 +1,6 @@
GIT
remote: git://github.com/floehopper/mocha.git
revision: 6fe08197f214457ac122c49d96fdac8e17adbea2
revision: 7dd9512a874f89b8037d7320fe7988c222f439e1
specs:
mocha (0.9.12.20110213002255)
@ -51,12 +51,12 @@ GEM
activesupport (= 3.0.7)
activesupport (3.0.7)
archive-tar-minitar (0.5.2)
arel (2.0.9)
arel (2.0.10)
autotest (4.4.6)
ZenTest (>= 4.4.1)
bcrypt-ruby (2.1.4)
bson (1.3.0)
bson_ext (1.3.0)
bson (1.3.1)
bson_ext (1.3.1)
builder (2.1.2)
bushido (0.0.12)
highline (>= 1.6.1)
@ -72,7 +72,7 @@ GEM
selenium-webdriver (>= 0.0.27)
xpath (~> 0.1.2)
celerity (0.8.9)
childprocess (0.1.8)
childprocess (0.1.9)
ffi (~> 1.0.6)
closure-compiler (1.1.1)
columnize (0.3.2)
@ -87,10 +87,10 @@ GEM
cucumber-rails (0.3.2)
cucumber (>= 0.8.0)
culerity (0.2.15)
custom_fields (1.0.0.beta.10)
activesupport (>= 3.0.4)
custom_fields (1.0.0.beta.12)
activesupport (>= 3.0.7)
locomotive_carrierwave
mongoid (~> 2.0.0.rc.7)
mongoid (~> 2.0.1)
daemons (1.1.3)
database_cleaner (0.6.7)
delayed_job (2.1.4)
@ -110,8 +110,7 @@ GEM
factory_girl_rails (1.0.1)
factory_girl (~> 1.3)
railties (>= 3.0.0)
ffi (1.0.7)
rake (>= 0.8.7)
ffi (1.0.8)
fog (0.3.7)
builder
excon (>= 0.2.3)
@ -134,8 +133,8 @@ GEM
heroku (1.19.1)
activesupport (>= 2.1.0)
launchy (~> 0.3.2)
rest-client (>= 1.4.0, < 1.7.0)
highline (1.6.1)
rest-client (< 1.7.0, >= 1.4.0)
highline (1.6.2)
httparty (0.7.7)
crack (= 0.1.8)
i18n (0.5.0)
@ -171,11 +170,11 @@ GEM
mime-types (1.16)
mimemagic (0.1.8)
mimetype-fu (0.1.2)
mongo (1.3.0)
bson (>= 1.3.0)
mongoid (2.0.0.rc.7)
mongo (1.3.1)
bson (>= 1.3.1)
mongoid (2.0.1)
activemodel (~> 3.0)
mongo (~> 1.2)
mongo (~> 1.3)
tzinfo (~> 0.3.22)
will_paginate (~> 3.0.pre)
net-ssh (2.0.24)
@ -205,7 +204,7 @@ GEM
rake (>= 0.8.7)
thor (~> 0.14.4)
rake (0.8.7)
responders (0.6.2)
responders (0.6.4)
rest-client (1.6.1)
mime-types (>= 1.16)
rmagick (2.12.2)
@ -276,7 +275,7 @@ DEPENDENCIES
capybara
cucumber (= 0.8.5)
cucumber-rails
custom_fields (= 1.0.0.beta.10)
custom_fields (= 1.0.0.beta.12)
database_cleaner
delayed_job (= 2.1.4)
delayed_job_mongoid (= 1.0.2)
@ -296,9 +295,9 @@ DEPENDENCIES
locomotive_mongoid_acts_as_tree (= 0.1.5.5)
mimetype-fu
mocha!
mongoid (= 2.0.0.rc.7)
mongoid (~> 2.0.1)
pickle
rails (>= 3.0.5)
rails (>= 3.0.7)
rmagick (= 2.12.2)
rspec-rails (= 2.3.1)
ruby-debug

View File

@ -19,8 +19,8 @@ h2. Gems
Here is a short list of main gems used in the application.
* Rails 3.0.5
* Mongoid 2.0.0.rc.7 (with MongoDB 1.6)
* Rails 3.0.7
* Mongoid 2.0.1 (with MongoDB 1.6)
* Liquid
* Devise
* Carrierwave
@ -38,6 +38,10 @@ h2. Team
* Contributors: "Dirk Kelly":http://www.dirkkelly.com, "Mario Visic":http://www.mariovisic.com, "Raphael Costa":http://raphaelcosta.net (Brazilian Portuguese translation), "Bernd Hauser":http://www.designhunger.de (German translation), "Andrea Frigido":http://www.frisoft.it (Italian translation)
* UI Designer: "Sacha Greif":http://www.sachagreif.com
h2. Support
Bernd Hauser from "designhunger":http://www.designhunger.de funded the following feature: *has_one* / *has_many* between content types.
h2. Credits
"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.

View File

@ -7,7 +7,7 @@ module Admin::ContentTypesHelper
@content_types = current_site.content_types.ordered.
limit(:contents => Locomotive.config.lastest_items_nb).
only(:name, :slug, :highlighted_field_name, :content_custom_fields_version).to_a
only(:name, :slug, :highlighted_field_name, :content_custom_fields_version, :order_by).to_a
if @content_type && @content_type.persisted? && @content_types.index(@content_type) >= MAX_DISPLAYED_CONTENTS
@content_types.delete(@content_type)

View File

@ -1,7 +1,7 @@
module Admin::CustomFieldsHelper
def options_for_field_kind
%w(string text category boolean date file).map do |kind|
%w(string text category boolean date file has_one has_many).map do |kind|
[t("custom_fields.kind.#{kind}"), kind]
end
end
@ -39,4 +39,19 @@ module Admin::CustomFieldsHelper
end
end
def options_for_association_target
current_site.content_types.collect { |c| [c.name, c.content_klass.to_s] }
end
def options_for_has_one(field)
target_contents_from_field(field).collect { |c| [c._label, c._id] }
end
alias :options_for_has_many :options_for_has_one
def target_contents_from_field(field)
content_type = field.target.constantize._parent
content_type.ordered_contents
end
end

View File

@ -5,6 +5,7 @@ class ContentInstance
## extensions ##
include CustomFields::ProxyClassEnabler
include Extensions::Shared::Seo
## fields (dynamic fields) ##
field :_slug
@ -29,6 +30,8 @@ class ContentInstance
## methods ##
delegate :site, :to => :content_type
alias :visible? :_visible?
alias :_permalink :_slug
@ -37,9 +40,11 @@ class ContentInstance
end
def highlighted_field_value
self.send(self.content_type.highlighted_field._name)
self.send(self.content_type.highlighted_field_name)
end
alias :_label :highlighted_field_value
def visible?
self._visible || self._visible.nil?
end
@ -48,6 +53,14 @@ class ContentInstance
Hash.new.replace(self.errors)
end
def reload_parent!
self.class.reload_parent!
end
def self.reload_parent!
self._parent = self._parent.reload
end
def to_liquid
Locomotive::Liquid::Drops::Content.new(self)
end

View File

@ -0,0 +1,13 @@
module Extensions
module Shared
module Seo
extend ActiveSupport::Concern
included do
field :meta_keywords, :type => String
field :meta_description, :type => String
end
end # Seo
end # Shared
end # Extensions

View File

@ -6,7 +6,7 @@ class Membership
field :admin, :type => Boolean, :default => false
## associations ##
referenced_in :account
referenced_in :account, :validate => false
embedded_in :site, :inverse_of => :memberships
## validations ##

View File

@ -10,6 +10,7 @@ class Page
include Extensions::Page::Templatized
include Extensions::Page::Redirect
include Extensions::Page::Listed
include Extensions::Shared::Seo
## fields ##
field :title

View File

@ -2,4 +2,9 @@
= include_javascripts :edit_custom_fields, :contents
= include_stylesheets :fancybox
= f.foldable_inputs :name => :meta do
= f.input :meta_keywords
= f.input :meta_description
= render 'admin/custom_fields/custom_form', :form => f, :title => :attributes, :parent => @content_type

View File

@ -4,35 +4,6 @@
- form.object.custom_fields.each do |field|
- required = highlighted_field_name == field._name || field.required
- if field.string?
= form.input field._alias.to_sym, :label => field.label, :hint => field.hint, :required => required
- elsif field.text?
= form.input field._alias.to_sym, :label => field.label, :hint => field.hint, :as => :text, :required => required, :input_html => { :class => field.text_formatting }
- elsif field.category?
= form.custom_input field._alias.to_sym, :label => field.label, :hint => field.hint, :css => 'toggle' do
= form.select field._name.to_sym, field.ordered_category_items.collect { |item| [item.name, item.id] }
%button.button.light.edit-categories-link{ :type => 'button', :'data-url' => edit_admin_custom_field_path(parent.class.model_name.underscore, parent.slug, field) }
%span!= t('.edit_categories')
- elsif field.boolean?
= form.custom_input field._alias.to_sym, :label => field.label, :hint => field.hint, :css => 'toggle' do
= form.check_box field._alias.to_sym
- elsif field.date?
= form.input field._alias.to_sym, :label => field.label, :hint => field.hint, :input_html => { :class => 'date' }
- elsif field.file?
= form.custom_input field._alias.to_sym, :label => field.label, :hint => field.hint, :css => 'file' do
= form.file_field field._name.to_sym
- if form.object.send(:"#{field._name}?")
%p.remove
%strong
= link_to File.basename(form.object.send(field._name).url), form.object.send(field._name).url
%span
&nbsp;/&nbsp;
!= t('.delete_file')
= form.check_box :"remove_#{field._name}"
= render "/admin/custom_fields/types/#{field.kind}", :form => form, :parent => parent, :field => field, :required => required
= render '/admin/custom_fields/category_tmpl'

View File

@ -8,6 +8,7 @@
= g.input :_alias
= g.input :hint
= g.input :text_formatting, :as => 'select', :collection => options_for_text_formatting, :include_blank => false, :wrapper_html => { :style => 'display: none' }
= g.input :target, :as => 'select', :collection => options_for_association_target, :include_blank => false, :wrapper_html => { :style => 'display: none' }
.popup-actions
%p

View File

@ -23,6 +23,8 @@
%input{ :name => '{{base_name}}[text_formatting]', :value => '{{{text_formatting}}}', :type => 'hidden', :'data-field' => 'text_formatting' }
%input{ :name => '{{base_name}}[target]', :value => '{{{target}}}', :type => 'hidden', :'data-field' => 'target' }
%input{ :name => '{{base_name}}[label]', :value => '{{{label}}}', :type => 'text', :'data-field' => 'label' }
&mdash;

View File

@ -0,0 +1,2 @@
= form.custom_input field._alias.to_sym, :label => field.label, :hint => field.hint, :css => 'toggle' do
= form.check_box field._alias.to_sym

View File

@ -0,0 +1,4 @@
= form.custom_input field._alias.to_sym, :label => field.label, :hint => field.hint, :css => 'toggle' do
= form.select field._name.to_sym, field.ordered_category_items.collect { |item| [item.name, item.id] }
%button.button.light.edit-categories-link{ :type => 'button', :'data-url' => edit_admin_custom_field_path(parent.class.model_name.underscore, parent.slug, field) }
%span!= t('.edit_categories')

View File

@ -0,0 +1 @@
= form.input field._alias.to_sym, :label => field.label, :hint => field.hint, :input_html => { :class => 'date' }

View File

@ -0,0 +1,10 @@
= form.custom_input field._alias.to_sym, :label => field.label, :hint => field.hint, :css => 'file' do
= form.file_field field._name.to_sym
- if form.object.send(:"#{field._name}?")
%p.remove
%strong
= link_to File.basename(form.object.send(field._name).url), form.object.send(field._name).url
%span
&nbsp;/&nbsp;
!= t('.delete_file')
= form.check_box :"remove_#{field._name}"

View File

@ -0,0 +1,33 @@
- field.target.constantize.reload_parent! # to make sure all the contents from the parent are loaded
= form.custom_input field._alias.to_sym, :label => field.label, :hint => field.hint, :css => 'has-many' do
.has-many-selector
%p{ :style => form.object.send(field._alias.to_sym).empty? ? '' : 'display: none' }
= t('.empty')
%ul
%script{ :type => 'text/x-mustache-template', :name => 'template', :'data-base-input-name' => "content[#{field._alias.to_sym}]" }
%li{ :class => "item {{behaviour_flag}}" }
%span.handle
= image_tag 'admin/form/icons/drag.png'
{{^if_template}}
%input{ :name => '{{base_name}}[]', :value => '{{{id}}}', :type => 'hidden', :'data-field' => 'id' }
{{/if_template}}
%strong {{label}}
{{#if_template}}
= select_tag 'label', ''
{{/if_template}}
%span.actions
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove'
%button{ :class => 'button light mini add', :type => 'button' }
%span!= t('admin.buttons.new_item')
%script{ :type => 'text/javascript', :name => 'data' }
!= collection_to_js(options_for_has_many(field), :taken_ids => form.object.send(field._alias.to_sym).ids)

View File

@ -0,0 +1,4 @@
- field.target.constantize.reload_parent! # to make sure all the contents from the parent are loaded
= form.input field._alias.to_sym, :label => field.label, :hint => field.hint, :input_html => { :class => 'has_one' }, :as => :select, :collection => options_for_has_one(field), :selected => form.object.send(field._alias.to_sym).try(:_id)

View File

@ -0,0 +1 @@
= form.input field._alias.to_sym, :label => field.label, :hint => field.hint, :required => required

View File

@ -0,0 +1 @@
= form.input field._alias.to_sym, :label => field.label, :hint => field.hint, :as => :text, :required => required, :input_html => { :class => field.text_formatting }

View File

@ -11,6 +11,10 @@
= f.input :slug, :required => false, :hint => @page.slug.blank? ? '&nbsp;' : page_url(@page), :input_html => { :data_url => get_path_admin_pages_url, :disabled => @page.index? || @page.not_found? }, :wrapper_html => { :style => "#{'display: none' if @page.templatized?}; height: 50px" }
= f.foldable_inputs :name => :meta do
= f.input :meta_keywords
= f.input :meta_description
= f.foldable_inputs :name => :advanced_options do

View File

@ -34,6 +34,7 @@ javascripts:
- public/javascripts/admin/plugins/fancybox.js
- public/javascripts/admin/plugins/mustache.js
- public/javascripts/admin/custom_fields/category.js
- public/javascripts/admin/custom_fields/has_many.js
asset_collections:
- public/javascripts/admin/plugins/fancybox.js
- public/javascripts/admin/plugins/mustache.js

View File

@ -50,10 +50,12 @@ en:
templatized: "Use the page as a template for a model you defined."
listed: "Control whether to show the page from generated menus."
content_type_id: "The type of content this page will be a template for."
meta_keywords: "Overrides the site's meta keywords used within the head tag of the page. They are separated by a comma."
meta_description: "Overrides the site's meta description used within the head tag of the page."
snippet:
slug: "You need to know it in order to insert the snippet inside a page"
site:
meta_keywords: "Meta keywords used within the head tag of the page. They are separeted by an empty space. Required for SEO."
meta_keywords: "Meta keywords used within the head tag of the page. They are separated by a comma. Required for SEO."
meta_description: "Meta description used within the head tag of the page. Required for SEO."
domain_name: "ex: locomotiveapp.org"
theme_asset:

View File

@ -53,10 +53,12 @@ fr:
templatized: "Utilise la page comme un template pour un modèle défini."
listed: "Controle si la page doit être visible depuis les menus automatiquement générés."
content_type_id: "Le type du contenu pour lequel cette page est un template."
meta_keywords: "Redéfinit les mots-clés du site. Utilisés à l'intérieur de la balise HEAD. Ils sont séparés par une virgule."
meta_description: "Redéfinit la description du site. Utilisée à l'intérieur de la balise HEAD."
snippet:
slug: "Utilisé pour insérer le snippet dans une page."
site:
meta_keywords: "Mots-clés utilisés à l'intérieur de la balise HEAD. Ils sont séparés par un espace. Requis pour un meilleur référencement."
meta_keywords: "Mots-clés utilisés à l'intérieur de la balise HEAD. Ils sont séparés par une virgule. Requis pour un meilleur référencement."
meta_description: "Description utilisée à l'intérieur de la balise HEAD. Requis pour un meilleur référencement."
domain_name: "ex: locomotiveapp.org"
theme_asset:

View File

@ -2,6 +2,7 @@ module Locomotive
module Liquid
module Drops
class Content < Base
delegate :meta_keywords, :meta_description, :to => "@source"
def before_method(meth)
return '' if @source.nil?

View File

@ -2,6 +2,7 @@ module Locomotive
module Liquid
module Drops
class Page < Base
delegate :meta_keywords, :meta_description, :to => "@source"
def title
@source.templatized? ? @context['content_instance'].highlighted_field_value : @source.title

View File

@ -5,8 +5,8 @@ module Locomotive
def render(context)
%{
<meta name="description" content="#{sanitized_string(context.registers[:site].meta_description)}" />
<meta name="keywords" content="#{sanitized_string(context.registers[:site].meta_keywords)}" />
<meta name="description" content="#{sanitized_string(meta_description(context))}" />
<meta name="keywords" content="#{sanitized_string(meta_keywords(context))}" />
}
end
@ -15,6 +15,19 @@ module Locomotive
string.strip.gsub(/"/, '')
end
def meta_description(context)
object = metadata_object(context)
object.try(:meta_description).blank? ? context.registers[:site].meta_description : object.meta_description
end
def meta_keywords(context)
object = metadata_object(context)
object.try(:meta_keywords).blank? ? context.registers[:site].meta_keywords : object.meta_keywords
end
def metadata_object(context)
context['content_instance'] || context['page']
end
end
::Liquid::Template.register_tag('seo_metadata', SEOMetadata)

View File

@ -17,10 +17,10 @@ Gem::Specification.new do |s|
s.required_rubygems_version = ">= 1.3.6"
s.rubyforge_project = "nowarning"
s.add_dependency "rails", ">= 3.0.6"
s.add_dependency "rails", ">= 3.0.7"
s.add_dependency "warden"
s.add_dependency "devise", "1.1.3"
s.add_dependency "mongoid", "2.0.0.rc.7"
s.add_dependency "mongoid", "2.0.1"
s.add_dependency "bson_ext", "~> 1.3.0"
s.add_dependency "locomotive_mongoid_acts_as_tree", "0.1.5.5"
s.add_dependency "will_paginate"
@ -37,7 +37,7 @@ Gem::Specification.new do |s|
s.add_dependency "rmagick", "2.12.2"
s.add_dependency "locomotive_carrierwave", "0.5.0.1.beta3"
s.add_dependency "custom_fields", "1.0.0.beta.10"
s.add_dependency "custom_fields", "1.0.0.beta.12"
s.add_dependency "fog", "0.3.7"
s.add_dependency "mimetype-fu"
s.add_dependency "actionmailer-with-request"

View File

@ -80,7 +80,7 @@ $(document).ready(function() {
$('#submenu > ul > li.' + css).addClass('on');
// form
$('.formtastic li input, .formtastic li textarea, .formtastic li code').focus(function() {
$('.formtastic li input, .formtastic li textarea, .formtastic li code, .formtastic li select').focus(function() {
$('.formtastic li.error:not(.code) p.inline-errors').fadeOut(200);
if ($(this).parent().hasClass('error')) {
$(this).nextAll('p.inline-errors').show();

View File

@ -50,7 +50,7 @@ $(document).ready(function() {
// edit
domField.find('a.edit').click(function(e) {
var link = $(this);
var attributes = ['_alias', 'hint', 'text_formatting'];
var attributes = ['_alias', 'hint', 'text_formatting', 'target'];
$.fancybox({
titleShow: false,
@ -76,6 +76,8 @@ $(document).ready(function() {
domBoxAttrVal(name, val);
});
if (domFieldVal(domField, 'kind').toLowerCase() == 'text') domBoxAttr('text_formatting').parents('li').show();
if (domFieldVal(domField, 'kind').toLowerCase() == 'has_one' ||
domFieldVal(domField, 'kind').toLowerCase() == 'has_many') domBoxAttr('target').parents('li').show();
}
});
e.preventDefault(); e.stopPropagation();
@ -200,161 +202,5 @@ $(document).ready(function() {
}
}
setup(); // <- let's the show begin
});
// $(document).ready(function() {
//
// $('fieldset.fields').parents('form').submit(function() {
// $('fieldset.fields li.template input, fieldset.fields li.template select').attr('disabled', 'disabled');
// });
//
// var defaultValue = $('fieldset.fields li.template input[type=text]').val();
// var selectOnChange = function(select) {
// select.hide();
// select.prev()
// .show()
// .html(select[0].options[select[0].options.selectedIndex].text);
// }
//
// var refreshPosition = function() {
// jQuery.each($('fieldset.fields li.added input.position'), function(index) {
// $(this).val(index);
// });
// }
//
// /* __ fields ___ */
// $('fieldset.fields li.added select').each(function() {
// var select = $(this)
// .hover(function() {
// clearTimeout(select.attr('timer'));
// }, function() {
// select.attr('timer', setTimeout(function() {
// select.hide();
// select.prev().show();
// }, 1000));
// })
// .change(function() { selectOnChange(select); });
//
// select.prev().click(function() {
// $(this).hide();
// select.show();
// });
// });
//
// $('fieldset.fields li.template input[type=text]').focus(function() {
// if ($(this).hasClass('void') && $(this).parents('li').hasClass('template'))
// $(this).val('').removeClass('void');
// });
//
// $('fieldset.fields li.template button').click(function() {
// var lastRow = $(this).parents('li.template');
//
// var label = lastRow.find('input.label').val().trim();
// if (label == '' || label == defaultValue) return false;
//
// var newRow = lastRow.clone(true).removeClass('template').addClass('added new').insertBefore(lastRow);
//
// var dateFragment = '[' + new Date().getTime() + ']';
// newRow.find('input, select').each(function(index) {
// $(this).attr('name', $(this).attr('name').replace('[-1]', dateFragment));
// });
//
// var select = newRow.find('select')
// .val(lastRow.find('select').val())
// .change(function() { selectOnChange(select); })
// .hover(function() {
// clearTimeout(select.attr('timer'));
// }, function() {
// select.attr('timer', setTimeout(function() {
// select.hide();
// select.prev().show();
// }, 1000));
// });
// select.prev()
// .html(select[0].options[select[0].options.selectedIndex].text)
// .click(function() {
// $(this).hide();
// select.show();
// });
//
// // then "reset" the form
// lastRow.find('input.label').val(defaultValue).addClass('void');
//
// // warn the sortable widget about the new row
// $("fieldset.fields ol").sortable('refresh');
//
// refreshPosition();
// });
//
// $('fieldset.fields li a.remove').click(function(e) {
// if (confirm($(this).attr('data-confirm'))) {
// var parent = $(this).parents('li');
//
// if (parent.hasClass('new'))
// parent.remove();
// else {
// var field = parent.find('input.position')
// field.attr('name', field.attr('name').replace('[position]', '[_destroy]'));
//
// parent.hide().removeClass('added')
// }
//
// refreshPosition();
// }
//
// e.preventDefault();
// e.stopPropagation();
// });
//
// // sortable list
// $("fieldset.fields ol").sortable({
// handle: 'span.handle',
// items: 'li:not(.template)',
// axis: 'y',
// update: refreshPosition
// });
//
// // edit in depth custom field
// $('fieldset.fields li.item span.actions a.edit').click(function() {
// var link = $(this);
// $.fancybox({
// titleShow: false,
// content: $(link.attr('href')).parent().html(),
// onComplete: function() {
// $('#fancybox-wrap form').submit(function(e) {
// $.fancybox.close();
// e.preventDefault();
// e.stopPropagation();
// });
//
// var parent = link.parent();
//
// if (parent.prevAll('select').val() == 'Text') {
// var formatting = parent.prevAll('.text-formatting').val();
// $('#fancybox-wrap #custom_fields_field_text_formatting').val(formatting);
// $('#fancybox-wrap #custom_fields_field_text_formatting_input').show();
// } else {
// $('#fancybox-wrap #custom_fields_field_text_formatting_input').hide();
// }
//
// var alias = parent.prevAll('.alias').val().trim();
// if (alias == '') alias = makeSlug(link.parent().prevAll('.label').val());
// $('#fancybox-wrap #custom_fields_field__alias').val(alias);
//
// var hint = parent.prevAll('.hint').val();
// $('#fancybox-wrap #custom_fields_field_hint').val(hint);
// },
// onCleanup: function() {
// var parent = link.parent();
//
// var alias = $('#fancybox-wrap #custom_fields_field__alias').val().trim();
// if (alias != '') parent.prevAll('.alias').val(alias);
// var hint = $('#fancybox-wrap #custom_fields_field_hint').val().trim();
// if (hint != '') parent.prevAll('.hint').val(hint);
// var formatting = $('#fancybox-wrap #custom_fields_field_text_formatting').val();
// parent.prevAll('.text-formatting').val(formatting);
// }
// })
// });
// });
setup(); // <- let the show begin
});

View File

@ -0,0 +1,161 @@
$(document).ready(function() {
// add/remove/sort items in a has_many relationship
$('.has-many-selector').hasManySelector();
});
(function($){
$.fn.hasManySelector = function(options) {
var populateSelect = function(context) {
context.select.find('option').remove();
for (var i = 0; i < context.data.collection.length; i++) {
var obj = context.data.collection[i];
if ($.inArray(obj[1], context.data.taken_ids) == -1)
context.select.append(new Option(obj[0], obj[1], true, true));
}
if (context.select.find('option').size() == 0)
context.list.find('li.template').hide();
else
context.list.find('li.template').show();
}
var addId = function(context, id) {
context.data.taken_ids.push(id);
populateSelect(context);
if (context.data.taken_ids.length > 0) {
context.empty.hide();
context.list.next('input[type=hidden]').remove();
}
if (context.data.taken_ids.length == context.data.collection.length)
context.sep.hide();
}
var removeId = function(context, id) {
context.data.taken_ids = jQuery.grep(context.data.taken_ids, function(value) {
return value != id;
});
populateSelect(context);
if (context.data.taken_ids.length == 0) {
context.empty.show();
context.list.after('<input type="hidden" name="' + context.baseInputName + '" value="" />');
}
context.sep.show();
}
var registerElementEvents = function(context, data, domElement) {
// remove
domElement.find('a.remove').click(function(e) {
domElement.remove();
removeId(context, data.id);
context.list.sortable('refresh');
e.preventDefault(); e.stopPropagation();
});
}
var registerElementTemplateEvents = function(context, domElement) {
// bind the "Add field" button
domElement.find('button').click(function(e) {
var newElement = {
id: context.select.val(),
label: context.select.find('option:selected').text()
};
addId(context, newElement.id);
addElement(context, newElement, { refreshPosition: true });
context.list.sortable('refresh');
e.preventDefault(); e.stopPropagation();
});
}
/* ___ Add an element into the list ___ */
var addElement = function(context, data, options) {
options = $.extend({
'is_template': false,
'refreshPosition': false
}, options);
data = $.extend({
behaviour_flag: function() { return options.is_template ? 'template' : 'added' },
base_name: function() { return options.is_template ? '' : context.baseInputName },
if_template: function() { return options.is_template }
}, data);
var html = Mustache.to_html(context.template, data);
var domElement = null;
if (options.is_template) {
domElement = context.list.append('<li class="sep">&nbsp;</li>').append(html).find('.template');
context.sep = context.list.find('.sep');
registerElementTemplateEvents(context, domElement);
}
else {
domElement = context.list.find('> .sep').before(html).prev('li');
registerElementEvents(context, data, domElement);
context.error.hide();
context.list.sortable('refresh');
}
}
return this.each(function() {
var wrapper = $(this);
var context = {
list: wrapper.find('ul'),
empty: wrapper.find('p:first'),
template: wrapper.find('script[name=template]').html(),
baseInputName: wrapper.find('script[name=template]').attr('data-base-input-name'),
data: eval(wrapper.find('script[name=data]').html()),
error: wrapper.parent().find('p.inline-errors')
};
// sortable list
context.list.sortable({
handle: 'span.handle',
items: 'li:not(.template)',
axis: 'y'
});
// add the template element used to insert the new ones
addElement(context, null, { is_template: true });
context.select = wrapper.find('select[name=label]');
populateSelect(context);
for (var i = 0; i < context.data.taken_ids.length; i++) {
var data = { id: context.data.taken_ids[i], label: null };
for (var j = 0; j < context.data.collection.length; j++) {
var current = context.data.collection[j];
if (data.id == current[1])
data.label = current[0];
}
addElement(context, data);
}
if (context.error.size() > 0)
context.error.show();
});
};
})(jQuery);

View File

@ -1,3 +1,7 @@
/* ___ AUTOMATICALLY GENERATED: see admin/button.scss for the source file */
/* ___ rounded ___ */
/* ___ box shadow ___ */
/* ___ others ___ */
.button {
display: inline-block;
background: transparent url(/images/admin/buttons/dark-gray-left.png) no-repeat 0 0;
@ -7,49 +11,51 @@
cursor: pointer;
border: none;
height: 31px;
outline: none;
}
.button span {
display: inline-block;
background: transparent url(/images/admin/buttons/dark-gray-right.png) no-repeat right top;
position: relative;
top: -1px;
padding: 3px 9px 9px 4px;
line-height: 21px;
text-shadow: 1px 1px 1px #000;
outline: none;
}
.button.light {
background-image: url(/images/admin/buttons/light-gray-left.png);
color: #787a89;
}
.button.light span {
background-image: url(/images/admin/buttons/light-gray-right.png);
text-shadow: 1px 1px 1px #fff;
}
.button.small {
background: #ebedf4;
outline: none;
-moz-border-radius : 10px;
-webkit-border-radius: 10px;
height: 20px;
font-size: 0.7em;
padding: 0px 12px 0px 12px;
color: #8B8D9A !important;
text-decoration: none;
text-shadow: 1px 1px 1px #fff;
}
.button.small.add {
}
.button.remove {
color: #ff092c !important;
font-size: 1.1em;
}
.button.remove:hover { text-decoration: underline; }
outline: none; }
.button span {
display: inline-block;
background: transparent url(/images/admin/buttons/dark-gray-right.png) no-repeat right top;
position: relative;
top: -1px;
padding: 3px 9px 9px 4px;
line-height: 21px;
text-shadow: 1px 1px 1px #000;
outline: none; }
.button.light {
background-image: url(/images/admin/buttons/light-gray-left.png);
color: #787a89; }
.button.light span {
background-image: url(/images/admin/buttons/light-gray-right.png);
text-shadow: 1px 1px 1px #fff; }
.button.small {
background: #ebedf4;
outline: none;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
height: 20px;
font-size: 0.7em;
padding: 0px 12px 0px 12px;
color: #8B8D9A !important;
text-decoration: none;
text-shadow: 1px 1px 1px #fff; }
.button.mini.add {
background: transparent;
height: 20px;
background: #e1e4ed;
background: -moz-linear-gradient(0% 100% 90deg, #d7dbe7, #ebedf4);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#ebedf4), to(#d7dbe7));
border-radius: 4px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.4);
-webkit-box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.4); }
.button.mini.add span {
background: none;
line-height: 10px;
padding: 0px 5px 0 0; }
.button.remove {
color: #ff092c !important;
font-size: 1.1em; }
.button.remove:hover {
text-decoration: underline; }

View File

@ -434,6 +434,85 @@ form.formtastic fieldset ol li.file p.remove a {
form.formtastic fieldset ol li.file p.remove a:hover { text-decoration: underline; }
form.formtastic fieldset ol li.has-many p {
font-style: italic;
margin-bottom: 5px;
}
form.formtastic fieldset ol li.has-many ul {
width: 736px;
margin-left: 20%;
}
form.formtastic fieldset ol li.has-many ul li.item {
background: transparent url(/images/admin/form/item-popup.png) no-repeat 0 0;
width: 413px;
margin: 0 0 10px 0;
}
form.formtastic fieldset ol li.has-many ul li.item strong {
font-size: 0.9em;
font-weight: bold;
color: #17171d;
position: relative;
top: -1px;
left: 10px;
}
form.formtastic fieldset ol li.has-many ul li.item span.handle {
cursor: move;
position: relative;
top: 1px;
}
form.formtastic fieldset ol li.has-many ul li.added span.actions a.remove {
display: inline;
}
form.formtastic fieldset ol li.has-many ul li.added span.actions button {
display: none;
}
form.formtastic fieldset ol li.has-many ul li.sep {
border-top: 1px solid #ccc;
height: 2px;
margin: 5px 0 7px;
width: 433px;
}
form.formtastic fieldset ol li.has-many ul li.template {}
form.formtastic fieldset ol li.has-many ul li.template select {
display: inline;
top: -1px;
}
form.formtastic fieldset ol li.has-many ul li.template span.handle {
display: none;
}
form.formtastic fieldset ol li.has-many ul li.template span.actions {
width: auto;
top: 0px;
}
form.formtastic fieldset ol li.has-many ul li.template span.actions a.remove {
display: none;
}
form.formtastic fieldset ol li.has-many ul li.template span.actions button {
display: inline;
position: relative;
top: 1px;
right: 3px;
}
form.formtastic fieldset ol li.has-many ul li.template span.actions button span {
font-size: 0.8em;
}
/* ___ my account ___ */
form.formtastic fieldset.language li.full span {

View File

@ -0,0 +1,126 @@
/* ___ AUTOMATICALLY GENERATED: see admin/button.scss for the source file */
@import "helpers";
.button {
display: inline-block;
background: transparent url(/images/admin/buttons/dark-gray-left.png) no-repeat 0 0;
padding: 0px 0px 0px 2px;
font-size: 0.9em;
color: white;
cursor: pointer;
border: none;
height: 31px;
outline: none;
span {
display: inline-block;
background: transparent url(/images/admin/buttons/dark-gray-right.png) no-repeat right top;
position: relative;
top: -1px;
padding: 3px 9px 9px 4px;
line-height: 21px;
text-shadow: 1px 1px 1px #000;
outline: none;
}
&.light {
background-image: url(/images/admin/buttons/light-gray-left.png);
color: #787a89;
span {
background-image: url(/images/admin/buttons/light-gray-right.png);
text-shadow: 1px 1px 1px #fff;
}
}
&.small {
background: #ebedf4;
outline: none;
-moz-border-radius : 10px;
-webkit-border-radius: 10px;
height: 20px;
font-size: 0.7em;
padding: 0px 12px 0px 12px;
color: #8B8D9A !important;
text-decoration: none;
text-shadow: 1px 1px 1px #fff;
}
&.mini.add {
background: transparent;
height: 20px;
@include linear-background-gradient(#ebedf4, #d7dbe7);
@include full-rounded(4px);
@include box-shadow(1px, 1px, 1px, rgba(0, 0, 0, 0.4));
span {
background: none;
line-height: 10px;
padding: 0px 5px 0 0;
}
}
&.remove {
color: #ff092c !important;
font-size: 1.1em;
&:hover { text-decoration: underline; }
}
}
// .button {
// display: inline-block;
// background: transparent url(/images/admin/buttons/dark-gray-left.png) no-repeat 0 0;
// padding: 0px 0px 0px 2px;
// font-size: 0.9em;
// color: white;
// cursor: pointer;
// border: none;
// height: 31px;
// outline: none;
// }
//
// .button span {
// display: inline-block;
// background: transparent url(/images/admin/buttons/dark-gray-right.png) no-repeat right top;
// position: relative;
// top: -1px;
// padding: 3px 9px 9px 4px;
// line-height: 21px;
// text-shadow: 1px 1px 1px #000;
// outline: none;
// }
//
// .button.light {
// background-image: url(/images/admin/buttons/light-gray-left.png);
// color: #787a89;
// }
//
// .button.light span {
// background-image: url(/images/admin/buttons/light-gray-right.png);
// text-shadow: 1px 1px 1px #fff;
// }
//
// .button.small {
// background: #ebedf4;
// outline: none;
// -moz-border-radius : 10px;
// -webkit-border-radius: 10px;
// height: 20px;
// font-size: 0.7em;
// padding: 0px 12px 0px 12px;
// color: #8B8D9A !important;
// text-decoration: none;
// text-shadow: 1px 1px 1px #fff;
// }
//
// .button.small.add {
// }
//
// .button.remove {
// color: #ff092c !important;
// font-size: 1.1em;
// }
//
// .button.remove:hover { text-decoration: underline; }

View File

@ -0,0 +1,27 @@
require 'spec_helper'
describe Locomotive::Liquid::Drops::Content do
before(:each) do
@site = Factory.build(:site)
content_type = Factory.build(:content_type)
content_type.content_custom_fields.build :label => 'anything', :kind => 'String'
@content = content_type.contents.build(:meta_keywords => 'Libidinous, Angsty', :meta_description => "Quite the combination.")
end
describe 'meta_keywords' do
subject { render_template('{{ content.meta_keywords }}') }
it { should == @content.meta_keywords }
end
describe 'meta_description' do
subject { render_template('{{ content.meta_description }}') }
it { should == @content.meta_description }
end
def render_template(template = '', assigns = {})
assigns = { 'content' => @content }.merge(assigns)
Liquid::Template.parse(template).render(::Liquid::Context.new({}, assigns, { :site => @site }))
end
end

View File

@ -3,7 +3,8 @@ require 'spec_helper'
describe Locomotive::Liquid::Drops::Page do
before(:each) do
@home = Factory.build(:page)
site = Factory.build(:site)
@home = Factory.build(:page, :site => site, :meta_keywords => 'Libidinous, Angsty', :meta_description => "Quite the combination.")
end
context '#rendering tree' do
@ -56,6 +57,16 @@ describe Locomotive::Liquid::Drops::Page do
end
describe 'meta_keywords' do
subject { render_template('{{ home.meta_keywords }}') }
it { should == @home.meta_keywords }
end
describe 'meta_description' do
subject { render_template('{{ home.meta_description }}') }
it { should == @home.meta_description }
end
def render_template(template = '', assigns = {})
assigns = {
'home' => @home

View File

@ -1,19 +1,17 @@
require 'spec_helper'
describe Locomotive::Liquid::Tags::SEOMetadata do
before :each do
@site = Factory.build(:site, :meta_description => 'A short site description', :meta_keywords => 'test only cat dog')
let(:site) do
Factory.build(:site, :meta_description => 'A short site description', :meta_keywords => 'test only cat dog')
end
context '#rendering' do
describe 'rendering' do
it 'renders a a meta description tag' do
render_seo_metadata.should include '<meta name="description" content="A short site description" />'
end
it 'strips and removes quote characters from the description' do
@site.meta_description = ' String with " " quotes '
site.meta_description = ' String with " " quotes '
render_seo_metadata.should include '<meta name="description" content="String with quotes" />'
end
@ -22,16 +20,53 @@ describe Locomotive::Liquid::Tags::SEOMetadata do
end
it 'strips and removes quote characters from the keywords' do
@site.meta_keywords = ' one " two " three '
site.meta_keywords = ' one " two " three '
render_seo_metadata.should include '<meta name="keywords" content="one two three" />'
end
context "when page" do
context "has metadata" do
let(:page) { site.pages.build(:meta_keywords => 'hulk,gamma', :meta_description => "Bruce Banner") }
subject { render_seo_metadata('page' => page) }
it { should include(%Q[<meta name="keywords" content="#{page.meta_keywords}" />]) }
it { should include(%Q[<meta name="description" content="#{page.meta_description}" />]) }
end
context "does not have metadata" do
let(:page) { site.pages.build }
subject { render_seo_metadata('page' => page) }
it { should include(%Q[<meta name="keywords" content="#{site.meta_keywords}" />]) }
it { should include(%Q[<meta name="description" content="#{site.meta_description}" />]) }
end
end
context "when content instance" do
let(:content_type) do
Factory.build(:content_type, :site => site).tap do |ct|
ct.content_custom_fields.build :label => 'anything', :kind => 'String'
end
end
context "has metadata" do
let(:content) { content_type.contents.build(:meta_keywords => 'Libidinous, Angsty', :meta_description => "Quite the combination.") }
subject { render_seo_metadata('content_instance' => content) }
it { should include(%Q[<meta name="keywords" content="#{content.meta_keywords}" />]) }
it { should include(%Q[<meta name="description" content="#{content.meta_description}" />]) }
end
context "does not have metadata" do
let(:content) { content_type.contents.build }
subject { render_seo_metadata('content_instance' => content) }
it { should include(%Q[<meta name="keywords" content="#{site.meta_keywords}" />]) }
it { should include(%Q[<meta name="description" content="#{site.meta_description}" />]) }
end
end
end
def render_seo_metadata
registers = { :site => @site }
liquid_context = ::Liquid::Context.new({}, {}, registers)
def render_seo_metadata(assigns={})
registers = { :site => site }
liquid_context = ::Liquid::Context.new({}, assigns, registers)
output = Liquid::Template.parse("{% seo_metadata %}").render(liquid_context)
end
end

View File

@ -93,6 +93,13 @@ describe ContentInstance do
end
end
describe '#site' do
it 'delegates to the content type' do
@content_type.expects(:site)
build_content.site
end
end
def build_content(options = {})
@content_type.contents.build({ :title => 'Locomotive', :description => 'Lorem ipsum....' }.merge(options))
@ -101,5 +108,4 @@ describe ContentInstance do
def fake_bson_id(id)
BSON::ObjectId(id.to_s.rjust(24, '0'))
end
end
end

View File

@ -229,8 +229,6 @@ describe Page do
@page.redirect_url = "invalid url with spaces"
@page.should_not be_valid
@page.errors[:redirect_url].should == ["is invalid"]
end
end
end