first cucumber features + crud for pages (95% done)

This commit is contained in:
dinedine 2010-05-09 14:44:53 +02:00
parent 3b847b2732
commit a0216dc75f
36 changed files with 744 additions and 152 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ db/*.sqlite3
log/*.log
tmp/**/*
.DS_Store
rerun.txt

View File

@ -7,7 +7,8 @@ gem "rails", "3.0.0.beta3"
gem "liquid"
gem "bson_ext", '0.20.1'
gem "mongo_ext"
gem "mongoid", ">= 2.0.0.beta2"
gem "mongoid", ">= 2.0.0.beta4"
gem "mongoid_acts_as_tree", :git => 'git://github.com/evansagge/mongoid_acts_as_tree.git'
gem "warden"
gem "devise", ">= 1.1.rc0"
gem "haml", '>= 3.0.0.beta.2', :git => 'git://github.com/nex3/haml.git'
@ -28,5 +29,9 @@ group :test do
gem 'rspec-rails', '>= 2.0.0.beta.5'
gem 'factory_girl', :git => 'git://github.com/thoughtbot/factory_girl.git', :branch => 'rails3'
gem 'capybara', :git => 'git://github.com/jnicklas/capybara.git'
gem 'cucumber-rails', :git => 'git://github.com/aslakhellesoy/cucumber-rails.git'
gem 'cucumber', '0.7.2'
gem 'cucumber-rails'
gem 'spork'
gem 'launchy'
gem 'mocha', :git => 'git://github.com/floehopper/mocha.git'
end

View File

@ -5,7 +5,7 @@ class Admin::LayoutsController < Admin::BaseController
def index
@layouts = current_site.layouts
end
def new
@layout = current_site.layouts.build
end

View File

@ -0,0 +1,10 @@
class Admin::PagePartsController < Admin::BaseController
layout nil
def index
parts = current_site.layouts.find(params[:layout_id]).parts
render :json => { :parts => parts }
end
end

View File

@ -8,10 +8,11 @@ class Admin::PagesController < Admin::BaseController
def new
@page = current_site.pages.build
@page.parts << PagePart.build_body_part
end
def edit
@page = current_site.pages.find(params[:id])
@page = current_site.pages.find(params[:id])
end
def create

View File

@ -6,4 +6,10 @@ class Admin::SessionsController < Devise::SessionsController
before_filter :require_site
protected
def after_sign_in_path_for(resource)
admin_pages_url
end
end

View File

@ -3,7 +3,6 @@ module Admin::PagesHelper
def parent_pages_options
roots = current_site.pages.roots.where(:slug.ne => '404').and(:_id.ne => @page.id)
puts roots.to_a.inspect
returning [] do |list|
roots.each do |page|
list = add_children_to_options(page, list)

View File

@ -1,10 +1,12 @@
class Layout < LiquidTemplate
## associations ##
has_many_related :pages
embeds_many :parts, :class_name => 'PagePart'
## callbacks ##
before_save :build_parts_from_value
after_save :update_parts_in_pages
## validations ##
validates_format_of :value, :with => Locomotive::Regexps::CONTENT_FOR_LAYOUT, :message => :missing_content_for_layout
@ -15,23 +17,31 @@ class Layout < LiquidTemplate
def build_parts_from_value
if self.value_changed? || self.new_record?
self.parts = []
self.parts.clear
(groups = self.value.scan(Locomotive::Regexps::CONTENT_FOR)).each do |part|
body = nil
self.value.scan(Locomotive::Regexps::CONTENT_FOR).each do |part|
part[1].strip!
part[1] = nil if part[1].empty?
slug = part[0].strip.downcase
name = (if slug == 'layout'
I18n.t('admin.shared.attributes.body')
if slug == 'layout'
body = PagePart.new :slug => slug, :name => I18n.t('admin.shared.attributes.body')
else
(part[1] || slug).gsub("\"", '')
end)
self.parts.build :slug => slug, :name => name
self.parts.build :slug => slug, :name => (part[1] || slug).gsub("\"", '')
end
end
self.parts.insert(0, body) if body
@_update_pages = true if self.value_changed?
end
end
def update_parts_in_pages
self.pages.each { |p| p.send(:update_parts!, self.parts) } if @_update_pages
end
end

View File

@ -13,6 +13,7 @@ class Page
## associations ##
belongs_to_related :site
belongs_to_related :layout
embeds_many :parts, :class_name => 'PagePart'
## callbacks ##
@ -22,7 +23,7 @@ class Page
before_save :change_parent
before_create { |p| p.fix_position(false) }
before_create :add_to_list_bottom
before_create :add_body_part
# before_create :add_body_part
before_destroy :remove_from_list
## validations ##
@ -34,6 +35,7 @@ class Page
## behaviours ##
acts_as_tree :order => ['position', 'asc']
# accepts_nested_attributes_for :parts, :allow_destroy => true
## methods ##
@ -45,8 +47,12 @@ class Page
self.slug == '404' && self.depth.to_i == 0
end
def parts_attributes=(attributes)
self.update_parts(attributes.values.map { |attrs| PagePart.new(attrs) })
end
def add_body_part
self.parts.build :name => 'body', :value => '---body here---'
self.parts.build :name => 'body', :slug => 'layout', :value => '---body here---'
end
def parent=(owner) # missing in acts_as_tree
@ -76,11 +82,35 @@ class Page
def ancestors
return [] if root?
self.class.find(self.path.clone << nil) # bug in mongoid (it does not handle array with on element)
self.class.find(self.path.clone << nil) # bug in mongoid (it does not handle array with one element)
end
protected
def update_parts(parts)
performed = []
# add / update
parts.each do |part|
if (existing = self.parts.detect { |p| p.id == part.id || p.slug == part.slug })
existing.attributes = part.attributes.delete_if { |k, v| %w{_id slug}.include?(k) }
else
self.parts << (existing = part)
end
performed << existing unless existing.disabled?
end
# disable missing parts
(self.parts.map(&:slug) - performed.map(&:slug)).each do |slug|
self.parts.detect { |p| p.slug == slug }.disabled = true
end
end
def update_parts!(new_parts)
self.update_parts(new_parts)
self.save
end
def change_parent
if self.parent_id_changed?
self.fix_position(false)

View File

@ -1,20 +1,36 @@
class PagePart
include Mongoid::Document
include Mongoid::Timestamps
# include Mongoid::Timestamps
## fields ##
field :name, :type => String
field :slug, :type => String
field :value, :type => String
field :disabled, :type => Boolean, :default => false
field :value, :type => String
## associations ##
embedded_in :page, :inverse_of => :parts
# attr_accessor :_delete
## callbacks ##
before_validate { |p| p.slug ||= p.name.slugify if p.name.present? }
# before_validate { |p| p.slug ||= p.name.slugify if p.name.present? }
## validations ##
validates_presence_of :name, :slug
## named scopes ##
named_scope :enabled, where(:disabled => false)
## methods ##
# def _delete=(value)
# puts "set _delete #{value.inspect}"
# self.attributes[:_destroy] = true if %w(t 1 true).include?(value)
# end
def self.build_body_part
self.new(:name => I18n.t('admin.shared.attributes.body'), :slug => 'layout')
end
end

View File

@ -1,7 +1,13 @@
- content_for :head do
= javascript_include_tag 'admin/plugins/codemirror/codemirror', 'admin/plugins/wslide', 'admin/pages', 'admin/page_parts'
= stylesheet_link_tag 'admin/page_parts'
= f.foldable_inputs :name => :information do
= f.input :title
= f.input :layout_id, :as => :select, :collection => current_site.layouts.all, :input_html => { :data_url => admin_layout_page_parts_url('_id_to_replace_') }
- if not @page.index? and not @page.not_found?
= f.input :parent_id, :as => :select, :collection => parent_pages_options, :include_blank => false
@ -15,57 +21,17 @@
= f.input :keywords
= f.input :description
#page-parts
.nav
- @page.parts.each_with_index do |part, index|
= link_to content_tag(:span, part.name), "#parts-#{index + 1}", :id => "control-part-#{part.slug}", :class => "part-#{index} #{'on' if index == 0}", :style => "#{'display: none' if part.disabled?}"
.clear
/ <% f.foldable_inputs :name => :information do %>
/ <%= f.input :title, :required => false %>
/
/ <% f.custom_input :path, :css => 'path' do %>
/ /<%= f.text_field :path %>.html
/ <% end %>
/
/ <% f.custom_input :layout do %>
/ <%= f.select :layout_id, current_site.layouts.all.collect { |l| [l.name, l.id] }, :include_blank => true %>
/ <% end %>
/
/ <%= f.input :keywords, :required => false %>
/ <%= f.input :description, :required => false %>
/
/ <% f.custom_input :cache_expires_in do %>
/ <%= f.select :cache_expires_in, options_for_page_cache_expiration %>
/ <% end %>
/
/ <% f.custom_input :visible, :css => 'checkbox' do %>
/ <%= f.check_box :visible %>
/ <% end %>
/ <% end %>
/
/ <% unless f.object.new_record? %>
/ <div id="page-parts">
/ <div class="jcarousel-control">
/ <% if body_part? %>
/ <a class="part-0 on"><%= t('admin.pages.form.body') %></a><span></span><% end -%><% @page.parts.active.each_with_index do |p, index| -%><a class="part-<%= index + (body_part? ? 1 : 0) %> <%= 'on' if !body_part? && index == 0 %>"><%= p.slug.humanize %></a><span></span>
/ <% end %>
/ </div>
/
/ <div class="wrapper">
/ <ul>
/ <% if body_part? %>
/ <li><code><%= f.text_area :body %></code></li>
/ <% end %>
/
/ <% f.fields_for :parts do |g| %>
/ <% unless g.object.disabled? %>
/ <li><code><%= g.text_area :body, :class => 'big' %></code></li>
/ <% end %>
/ <% end %>
/ </ul>
/ </div>
/ </div>
/
/ <% content_for :head do %>
/ <%= javascript_include_tag 'admin/plugins/iphoneSwitch', 'admin/plugins/checkbox', 'admin/plugins/carousel', 'codemirror/codemirror', 'admin/pages' %>
/ <%= stylesheet_link_tag 'carousel', 'admin/page_parts' %>
/ <% end %>
/ <% end %>
.wrapper
%ul{ :id => "parts" }
= f.fields_for :parts do |g|
%li{ :style => "#{'display: none' if g.object.disabled?}" }
%code= g.text_area :value
= g.hidden_field :name
= g.hidden_field :slug
= g.hidden_field :disabled, :class => 'disabled'

View File

@ -1,8 +1,5 @@
- title link_to(@page.title.blank? ? @page.title_was : @page.title, '#', :rel => 'page_title', :title => t('.ask_for_title'), :class => 'editable')
- content_for :head do
= javascript_include_tag 'admin/pages'
- content_for :submenu do
= render 'admin/shared/menu/contents'

View File

@ -1,8 +1,5 @@
- title t('.title')
- content_for :head do
= javascript_include_tag 'admin/pages'
- content_for :submenu do
= render 'admin/shared/menu/contents'

View File

@ -1,7 +1,8 @@
<%
rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : ""
rerun_opts = rerun.to_s.strip.empty? ? "--format progress features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}"
std_opts = "#{rerun_opts} --format rerun --out rerun.txt --strict --tags ~@wip"
rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}"
std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} --strict --tags ~@wip"
%>
default: <%= std_opts %>
default: <%= std_opts %> features
wip: --tags @wip:3 --wip features
rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip

View File

@ -5,7 +5,7 @@ development:
<<: *defaults
database: locomotive_dev
test:
test: &test
<<: *defaults
database: locomotive_test
@ -15,3 +15,6 @@ production:
username: user
password: pass
database: fanboy
cucumber:
<<: *test

View File

@ -14,8 +14,10 @@ end
## various patches
# Enabling scope in validates_uniqueness_of validation
module Mongoid #:nodoc:
# Enabling scope in validates_uniqueness_of validation
module Validations #:nodoc:
class UniquenessValidator < ActiveModel::EachValidator
def validate_each(document, attribute, value, scope = nil)
@ -26,4 +28,14 @@ module Mongoid #:nodoc:
end
end
end
end
# FIX BUG #71 http://github.com/durran/mongoid/commit/47a97094b32448aa09965c854a24c78803c7f42e
module Associations
module InstanceMethods
def update_embedded(name)
association = send(name)
association.to_a.each { |doc| doc.save if doc.changed? || doc.new_record? } unless association.blank?
end
end
end
end

View File

@ -20,7 +20,7 @@ en:
messages:
confirm: Are you sure ?
shared:
header:
welcome: Welcome, {{name}}
@ -48,6 +48,9 @@ en:
title: Listing pages
no_items: "There are no pages for now. Just click <a href=\"{{url}}\">here</a> to create the first one."
new: new page
messages:
successful_create: "Page was successfully created."
successful_update: "Page was successfully updated."
layouts:
index:

View File

@ -22,7 +22,9 @@ Locomotive::Application.routes.draw do |map|
get :get_path, :on => :collection
end
resources :layouts
resources :layouts do
resources :page_parts, :only => :index
end
resources :snippets
# get 'login' => 'sessions#new', :as => :new_account_session

View File

@ -15,14 +15,19 @@ x store node closed or open in cookies
x snippets section
x menu items have to be translated
x layout needs at least content_for_layout
- validates_uniqueness_of :slug, :scope => :id
- page parts
- can not delete index + 404 pages
x parts js/css:
x codemirror
x change bg (separator)
x when a tab is selected, if we change layout, we should move to the first visible one
x page parts
x layout part should be always in first
x pages section (CRUD)
- slug unique within a folder
- pages section (CRUD)
- validates_uniqueness_of :slug, :scope => :id
- refactoring page.rb => create module pagetree
- my account section (part of settings)
- layouts section
- create 404 + index pages once a site is created
- can not delete index + 404 pages
- refactoring admin crud (pages + layouts + snippets)
- remove all pages, snippets, ...etc when destroying a website

View File

@ -0,0 +1,19 @@
@site_up
Feature: Login
In order to access locomotive admin panel
As an administrator
I want to log in
Scenario: Successful authentication
When I go to login
And I fill in "account_email" with "admin@locomotiveapp.org"
And I fill in "account_password" with "easyone"
And I press "Log in"
Then I should see "Listing pages"
Scenario: Failed authentication
When I go to login
And I fill in "account_email" with "admin@locomotiveapp.org"
And I fill in "account_password" with ""
And I press "Log in"
Then I should not see "Listing pages"

View File

@ -0,0 +1,31 @@
@site_up
@authenticated
Feature: Manage Skills
In order to manage pages
As an administrator
I want to add/edit/delete pages of my site
Scenario: Pages list is not accessible for non authenticated accounts
Given I am not authenticated
When I go to pages
Then I should see "Login"
Scenario: Creating a valid page
When I go to pages
And I follow "new page"
And I fill in "Title" with "Test"
And I fill in "Slug" with "test"
And I select "Home page" from "Parent"
And I fill in "page_parts_attributes_0_value" with "Lorem ipsum...."
And I press "Create"
Then I should see "Page was successfully created."
And I should have "Lorem ipsum...." in the test page layout
Scenario: Updating a valid page
When I go to pages
And I follow "Home page"
And I fill in "Title" with "Home page !"
And I fill in "page_parts_attributes_0_value" with "My new content is here"
And I press "Update"
Then I should see "Page was successfully updated."
And I should have "My new content is here" in the index page layout

View File

@ -0,0 +1,63 @@
Before('@site_up') do
create_site_and_admin_account
create_layout_samples
create_index_page
end
Before('@authenticated') do
Given %{I am an authenticated user}
end
### Authentication
Given /^I am not authenticated$/ do
visit('/admin/logout')
end
Given /^I am an authenticated user$/ do
Given %{I go to login}
And %{I fill in "account_email" with "admin@locomotiveapp.org"}
And %{I fill in "account_password" with "easyone"}
And %{I press "Log in"}
end
Then /^I am redirected to "([^\"]*)"$/ do |url|
assert [301, 302].include?(@integration_session.status), "Expected status to be 301 or 302, got #{@integration_session.status}"
location = @integration_session.headers["Location"]
assert_equal url, location
visit location
end
### Pages
Then /^I should have "(.*)" in the (.*) page (.*)$/ do |content, page_slug, slug|
page = @site.pages.where(:slug => page_slug).first
part = page.parts.where(:slug => slug).first
part.should_not be_nil
part.value.should == content
end
## Common
def create_site_and_admin_account
@site = Factory(:site, :name => 'Locomotive test website', :subdomain => 'test')
@admin = Factory(:account, { :name => 'Admin', :email => 'admin@locomotiveapp.org' })
end
def create_layout_samples
Factory(:layout, :site => @site, :name => 'One column', :value => %{<html>
<head>
<title>My website</title>
</head>
<body>
<div id="main">\{\{ content_for_layout \}\}</div>
</body>
</html>})
Factory(:layout, :site => @site)
end
def create_index_page
Factory(:page, :site => @site, :layout => @site.layouts.last, :parts => [PagePart.new(:slug => 'layout', :name => 'Body', :value => 'Hello world')])
end

View File

@ -6,6 +6,7 @@
require 'uri'
require 'cgi'
require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
module WithinHelpers
@ -147,7 +148,8 @@ end
Then /^the "([^\"]*)" field(?: within "([^\"]*)")? should contain "([^\"]*)"$/ do |field, selector, value|
with_scope(selector) do
field_value = find_field(field).value
field = find_field(field)
field_value = (field.tag_name == 'textarea') ? field.text : field.value
if field_value.respond_to? :should
field_value.should =~ /#{value}/
else
@ -158,11 +160,12 @@ end
Then /^the "([^\"]*)" field(?: within "([^\"]*)")? should not contain "([^\"]*)"$/ do |field, selector, value|
with_scope(selector) do
field_value = find_field(field).value
field = find_field(field)
field_value = (field.tag_name == 'textarea') ? field.text : field.value
if field_value.respond_to? :should_not
field_value.should_not =~ /#{value}/
else
assert_no_match(/#{value}/, field_value.value)
assert_no_match(/#{value}/, field_value)
end
end
end
@ -199,9 +202,11 @@ Then /^(?:|I )should be on (.+)$/ do |page_name|
end
Then /^(?:|I )should have the following query string:$/ do |expected_pairs|
actual_params = CGI.parse(URI.parse(current_url).query)
expected_params = Hash[expected_pairs.rows_hash.map{|k,v| [k,[v]]}]
query = URI.parse(current_url).query
actual_params = query ? CGI.parse(query) : {}
expected_params = {}
expected_pairs.rows_hash.each_pair{|k,v| expected_params[k] = v.split(',')}
if actual_params.respond_to? :should
actual_params.should == expected_params
else

View File

@ -10,7 +10,6 @@ require File.expand_path(File.dirname(__FILE__) + '/../../config/environment')
require 'cucumber/formatter/unicode' # Remove this line if you don't want Cucumber Unicode support
require 'cucumber/rails/rspec'
require 'cucumber/rails/world'
require 'cucumber/rails/active_record'
require 'cucumber/web/tableish'
require 'capybara/rails'
@ -34,26 +33,27 @@ Capybara.default_selector = :css
# of your scenarios, as this makes it hard to discover errors in your application.
ActionController::Base.allow_rescue = false
# If you set this to true, each scenario will run in a database transaction.
# You can still turn off transactions on a per-scenario basis, simply tagging
# a feature or scenario with the @no-txn tag. If you are using Capybara,
# tagging with @culerity or @javascript will also turn transactions off.
#
# If you set this to false, transactions will be off for all scenarios,
# regardless of whether you use @no-txn or not.
#
# Beware that turning transactions off will leave data in your database
# after each scenario, which can lead to hard-to-debug failures in
# subsequent scenarios. If you do this, we recommend you create a Before
# block that will explicitly put your database in a known state.
Cucumber::Rails::World.use_transactional_fixtures = true
require 'factory_girl'
require 'spec/factories'
# How to clean your database when transactions are turned off. See
# http://github.com/bmabey/database_cleaner for more info.
if defined?(ActiveRecord::Base)
begin
require 'database_cleaner'
DatabaseCleaner.strategy = :truncation
rescue LoadError => ignore_if_database_cleaner_not_present
end
Before do
Mongoid.master.collections.each(&:drop)
end
Locomotive.configure do |config|
config.default_domain = 'example.com'
end
# class ActionController::Integration::Session
# def reset_with_test_subdomain!
# self.reset_without_test_subdomain!
# self.host = "test.example.com"
# end
# alias_method_chain :reset!, :test_subdomain
# end
#
# class ActionDispatch::Integration::Session
# DEFAULT_HOST = 'test.example.com'
# end
Capybara.default_host = 'test.example.com'

View File

@ -7,10 +7,16 @@ module NavigationHelpers
#
def path_to(page_name)
case page_name
when /the home\s?page/
'/'
when /login/
new_account_session_path
when /logout/
destroy_account_session_path
when /pages/
admin_pages_path
# Add more mappings here.
# Here is an example that pulls values out of the Regexp:
#
@ -18,8 +24,14 @@ module NavigationHelpers
# user_profile_path(User.find_by_login($1))
else
raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
"Now, go and add a mapping in #{__FILE__}"
begin
page_name =~ /the (.*) page/
path_components = $1.split(/\s+/)
self.send(path_components.push('path').join('_').to_sym)
rescue Object => e
raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
"Now, go and add a mapping in #{__FILE__}"
end
end
end
end

View File

@ -14,18 +14,24 @@ begin
require 'cucumber/rake/task'
namespace :cucumber do
Cucumber::Rake::Task.new({:ok => 'db:test:prepare'}, 'Run features that should pass') do |t|
Cucumber::Rake::Task.new(:ok, 'Run features that should pass') do |t|
t.binary = vendored_cucumber_bin # If nil, the gem's binary is used.
t.fork = true # You may get faster startup if you set this to false
t.profile = 'default'
end
Cucumber::Rake::Task.new({:wip => 'db:test:prepare'}, 'Run features that are being worked on') do |t|
Cucumber::Rake::Task.new(:wip, 'Run features that are being worked on') do |t|
t.binary = vendored_cucumber_bin
t.fork = true # You may get faster startup if you set this to false
t.profile = 'wip'
end
Cucumber::Rake::Task.new(:rerun, 'Record failing features and run only them if any exist') do |t|
t.binary = vendored_cucumber_bin
t.fork = true # You may get faster startup if you set this to false
t.profile = 'rerun'
end
desc 'Run all features'
task :all => [:ok, :wip]
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

View File

@ -24,7 +24,7 @@ var addCodeMirrorEditor = function(type, el, parser) {
if (type == 'liquid') type = 'xml';
var editor = CodeMirror.fromTextArea(el.attr('id'), {
height: "330px",
height: "400px",
parserfile: parserfile,
stylesheet: ["/stylesheets/admin/plugins/codemirror/" + type + "colors.css", "/stylesheets/admin/plugins/codemirror/liquidcolors.css"],
path: "/javascripts/admin/plugins/codemirror/",

View File

@ -0,0 +1,83 @@
$(document).ready(function() {
// slider
var resetSlider = function() {
$('#page-parts .wrapper ul').wslide({
width: 880,
height: 400,
autolink: false,
duration: 300,
horiz: true
});
}
resetSlider();
// codemirror
$('#parts code textarea').each(function() { addCodeMirrorEditor('liquid', $(this)); });
var refreshParts = function(parts) {
console.log('refreshParts');
$('#page-parts .nav a').removeClass('enabled');
$(parts).each(function() {
console.log("iterating..." + this.slug);
var control = $('#control-part-' + this.slug);
// adding missing part
if (control.size() == 0) {
console.log('adding part');
var nbParts = $('#page-parts .nav a').size();
$('#page-parts .nav .clear').before('<a id="control-part-' + this.slug + '" class="enabled part-' + nbParts + '" href="#parts-' + (nbParts + 1) + '"><span>' + this.name + '</span></a>');
var textareaInput = '<textarea rows="20" name="page[parts_attributes][' + nbParts + '][value]" id="page_parts_attributes_' + nbParts + '_value"/>';
var hiddenInputs = '<input type="hidden" name="page[parts_attributes][' + nbParts + '][name]" value="' + this.name + '" />'
hiddenInputs += '<input type="hidden" name="page[parts_attributes][' + nbParts + '][slug]" value="' + this.slug + '" />'
$('#page-parts .wrapper ul').append('<li id="part-' + nbParts + '" class="new"><code>' + textareaInput + '</code>' + hiddenInputs + '</li>');
resetSlider();
$('#parts li:last code textarea').each(function() { addCodeMirrorEditor('liquid', $(this)); });
} else {
var index = parseInt(control.attr('class').match(/part-(.+)/)[1]) + 1;
var wrapper = $('#parts-' + index);
// updating part
control.html('<span>' + this.name + '</span>').addClass('enabled').show();
wrapper.find('input.disabled').val('false');
wrapper.show();
}
});
// removing or hiding parts
$('#page-parts .nav a:not(.enabled)').each(function() {
var index = parseInt($(this).attr('class').match(/part-(.+)/)[1]) + 1;
var wrapper = $('#parts-' + index);
if (wrapper.hasClass('new')) {
$(this).remove(); wrapper.remove();
} else {
wrapper.find('input.disabled').val('true');
$(this).hide(); wrapper.hide();
}
});
// go to the first one if we hid the selected wrapper
var selectedNav = $('#page-parts .nav a.on');
if (selectedNav.size() == 1) {
var index = parseInt(selectedNav.attr('class').match(/part-(.+)/)[1]) + 1;
if ($('#parts-' + index + ':visible').size() == 0)
$('#page-parts .nav a:first').click();
} else
$('#page-parts .nav a:first').click();
}
var loadPartsFromLayout = function() {
if ($('#page_layout_id').val() == '')
return ;
var url = $('#page_layout_id').attr('data_url').replace('_id_to_replace_', $('#page_layout_id').val());
$.get(url, '', function(data) { refreshParts(data.parts) }, 'json');
}
$('#page_layout_id').change(loadPartsFromLayout);
});

View File

@ -0,0 +1,125 @@
/**
* wSlide 0.1 - http://www.webinventif.fr/wslide-plugin/
*
* Rendez vos sites glissant !
*
* Copyright (c) 2008 Julien Chauvin (webinventif.fr)
* Licensed under the Creative Commons License:
* http://creativecommons.org/licenses/by/3.0/
*
* Date: 2008-01-27
*/
(function($){
$.fn.wslide = function(h) {
h = jQuery.extend({
width: 150,
height: 150,
pos: 1,
col: 1,
effect: 'swing',
fade: false,
horiz: false,
autolink: true,
duration: 1500
}, h);
function gogogo(g){
g.each(function(i){
var a = $(this);
var uniqid = a.attr('id');
if(uniqid == undefined){
uniqid = 'wslide'+i;
}
if ($(this).parent().hasClass('wslide-wrap'))
a = $(this).parent();
else {
$(this).wrap('<div class="wslide-wrap" id="'+uniqid+'-wrap"></div>');
a = $('#'+uniqid+'-wrap');
}
var b = a.find('ul li');
var effets = h.effect;
if(jQuery.easing.easeInQuad == undefined && (effets!='swing' || effets!='normal')){
effets = 'swing';
}
var typex = h.width;
var typey = h.height;
function resultante(prop){
var tempcalc = prop;
tempcalc = tempcalc.split('px');
tempcalc = tempcalc[0];
return Number(tempcalc);
}
var litypex = typex-(resultante(b.css('padding-left'))+resultante(b.css('padding-right')));
var litypey = typey-(resultante(b.css('padding-top'))+resultante(b.css('padding-bottom')));
var col = h.col;
if(h.horiz){
col = Number(b.length+1);
}
var manip = '';
var ligne = Math.ceil(Number(b.length)/col);
a.css('overflow','hidden').css('position','relative').css('text-align','left').css('height',typey+'px').css('width',typex+'px').css('margin','0').css('padding','0');
a.find('ul').css('position','absolute').css('margin','0').css('padding','0').css('width',Number((col+0)*typex)+'px').css('height',Number(ligne*typey)+'px');
b.css('display','block').css('overflow','hidden').css('float','left').css('height',litypey+'px').css('width',litypex+'px');
b.each(function (i) {
var offset = a.offset();
var thisoffset = $(this).offset();
$(this).attr('id',uniqid+'-'+Number(i+1)).attr('rel', Number(thisoffset.left-offset.left)+':'+Number(thisoffset.top-offset.top));
manip += ' <a href="#'+uniqid+'-'+Number(i+1)+'">'+Number(i+1)+'</a>';
});
if(typeof h.autolink == 'boolean'){
if(h.autolink){
a.after('<div class="wslide-menu" id="'+uniqid+'-menu">'+manip+'</div>');
}
}else if (typeof h.autolink == 'string'){
if($('#'+h.autolink).length){
$('#'+h.autolink).html(manip);
}else{
a.after('<div id="#'+h.autolink+'">'+manip+'</div>');
}
}
var start = '#'+uniqid+'-';
var stoccurent = "";
$('a[href*="'+start+'"]').unbind('click').click(function () {
$('a[href*="'+stoccurent+'"]').removeClass("on");
$(this).addClass("on");
var tri = $(this).attr('href');
tri=tri.split('#');
tri='#'+tri[1];
stoccurent = tri;
var decal = $(tri).attr('rel');
decal = decal.split(':');
var decal2 = decal[1];
decal2 = -decal2;
decal = decal[0];
decal = -decal;
if(h.fade){
a.find('ul').animate({ opacity: 0 }, h.duration/2, effets, function(){$(this).css('top',decal2+'px').css('left',decal+'px');$(this).animate({ opacity: 1 }, h.duration/2, effets)} );
}else{
a.find('ul').animate({ top: decal2+'px',left: decal+'px' }, h.duration, effets );
}
return false;
});
if(h.pos <= 0){
h.pos = 1;
}
$('a[href$="'+start+h.pos+'"]').addClass("on");
var tri = $('a[href*="'+start+'"]:eq('+Number(h.pos-1)+')').attr('href');
tri=tri.split('#');
tri='#'+tri[1];
stoccurent = tri;
var decal = $(tri).attr('rel');
decal = decal.split(':');
var decal2 = decal[1];
decal2 = -decal2;
decal = decal[0];
decal = -decal;
a.find('ul').css('top',decal2+'px').css('left',decal+'px');
})
}
gogogo(this);
return this;
}
})(jQuery);

View File

@ -0,0 +1,60 @@
#page-parts .wrapper {
background: #ebedf4 url(/images/admin/form/footer.png) no-repeat 0 bottom;
width: 880px;
padding: 20px 20px;
}
#page-parts {
background: transparent url(/images/admin/form/header.png) no-repeat 0 0;
}
#page-parts .control {
height: 30px;
}
#page-parts .nav a {
float: left;
display: block;
height: 30px;
padding: 0px 0px 0 11px;
color: #8b8d9a;
text-decoration: none;
font-size: 0.8em;
cursor: pointer;
outline: none;
}
#page-parts .nav a span {
display: inline-block;
height: 26px;
padding: 4px 11px 0 0px;
}
#page-parts .nav a:first-child { padding-left: 22px; }
#page-parts .nav a:first-child span { padding-right: 13px; }
#page-parts .nav a.on {
color: #1e1f26;
font-weight: bold;
background: transparent url(/images/admin/form/header-left-on.png) no-repeat 0 0;
}
#page-parts .nav a.on span {
background: transparent url(/images/admin/form/header-right-on.png) no-repeat right 0;
}
#page-parts .nav a:first-child.on {
background: transparent url(/images/admin/form/header-first-on.png) no-repeat 0 0;
}
#page-parts .wslide-wrap {
border: 1px solid #a6a8b8;
}
#page-parts code { display: block; background: white; height: 400px; }
#page-parts code textarea {
width: 880px;
height: 400px;
background: transparent url(../../images/admin/form/field.png) repeat-x 0 0;
border: 0px;
}

View File

@ -35,11 +35,11 @@ Factory.define :layout do |l|
l.name '1 main column + sidebar'
l.value %{<html>
<head>
<title>Hello world !</title>
<title>My website</title>
</head>
<body>
<div id="main">\{\{ content_for_layout \}\}</div>
<div id="sidebar">\{\{ content_for_sidebar "Left Sidebar"\}\}</div>
<div id="main">\{\{ content_for_layout \}\}</div>
</body>
</html>}
end

View File

@ -14,13 +14,14 @@ describe Layout do
layout.errors[:value].should == ["should contain 'content_for_layout' liquid tag"]
end
describe 'once created' do
describe 'page parts' do
before(:each) do
@layout = Factory(:layout)
@layout = Factory.build(:layout)
end
it 'should have 2 parts' do
@layout.send(:build_parts_from_value)
@layout.parts.count.should == 2
@layout.parts.first.name.should == 'Body'
@ -30,6 +31,22 @@ describe Layout do
@layout.parts.last.slug.should == 'sidebar'
end
it 'should not add parts to pages if layout does not change' do
@layout.stubs(:value_changed?).returns(false)
page = Factory.build(:page, :layout => @layout, :site => nil)
page.expects(:update_parts!).never
@layout.pages << page
@layout.save
end
it 'should add parts to pages if layout changes' do
@layout.value = @layout.value + "..."
page = Factory.build(:page, :layout => @layout, :site => nil)
page.expects(:update_parts!)
@layout.pages << page
@layout.save
end
end
end

View File

@ -48,13 +48,7 @@ describe Page do
Factory.build(:page, :slug => 'index', :site => nil).index?.should be_true
Factory.build(:page, :slug => 'index', :depth => 1, :site => nil).index?.should be_false
end
it 'should add the body part' do
page = Factory(:page)
page.parts.should_not be_empty
page.parts.first.name.should == 'body'
end
it 'should have normalized slug' do
page = Factory.build(:page, :slug => ' Valid ité.html ')
page.valid?
@ -67,6 +61,120 @@ describe Page do
end
describe 'accepts_nested_attributes_for used for parts' do
before(:each) do
@page = Factory.build(:page)
@page.parts.build(:name => 'Main content', :slug => 'layout')
@page.parts.build(:name => 'Left column', :slug => 'left_sidebar')
@page.parts.build(:name => 'Right column', :slug => 'right_sidebar')
end
it 'should add parts' do
attributes = { '0' => { 'slug' => 'footer', 'name' => 'A custom footer', 'value' => 'End of page' } }
@page.parts_attributes = attributes
@page.parts.size.should == 4
@page.parts.last.slug.should == 'footer'
@page.parts.last.disabled.should == false
end
it 'should update parts' do
attributes = { '0' => { 'slug' => 'layout', 'name' => 'A new name', 'value' => 'Hello world' } }
@page.parts_attributes = attributes
@page.parts.size.should == 3
@page.parts.first.slug.should == 'layout'
@page.parts.first.name.should == 'A new name'
@page.parts.first.value.should == 'Hello world'
end
it 'should disable parts' do
attributes = { '0' => { 'slug' => 'left_sidebar', 'disabled' => 'true' } }
@page.parts_attributes = attributes
@page.parts.size.should == 3
@page.parts.first.disabled.should == true
@page.parts[1].disabled.should == true
@page.parts[2].disabled.should == true
end
it 'should add/update/disable parts at the same time' do
@page.parts.size.should == 3
attributes = {
'0' => { 'slug' => 'layout', 'name' => 'Body', 'value' => 'Hello world' },
'1' => { 'slug' => 'left_sidebar', 'disabled' => 'true' },
'2' => { 'id' => @page.parts[2].id, 'value' => 'Content from right sidebar', 'disabled' => 'false' }
}
@page.parts_attributes = attributes
@page.parts.size.should == 3
@page.parts[0].value.should == 'Hello world'
@page.parts[1].disabled.should == true
@page.parts[2].disabled.should == false
end
it 'should update it with success (mongoid bug #71)' do
@page.save
@page = Page.first
@page.parts.size.should == 3
@page.parts_attributes = { '0' => { 'slug' => 'header', 'name' => 'A custom header', 'value' => 'Head of page' } }
@page.parts.size.should == 4
@page.save
@page = Page.first
@page.parts.size.should == 4
end
end
describe 'dealing with page parts' do # DUPLICATED ?
before(:each) do
@page = Factory.build(:page)
@parts = [
PagePart.new(:name => 'Main content', :slug => 'layout'),
PagePart.new(:name => 'Left column', :slug => 'left_sidebar'),
PagePart.new(:name => 'Right column', :slug => 'right_sidebar')
]
@page.send(:update_parts, @parts)
end
it 'should add new parts from an array of parts' do
@page.parts.size.should == 3
@page.parts.shift.name.should == 'Main content'
@page.parts.shift.name.should == 'Left column'
@page.parts.shift.name.should == 'Right column'
end
it 'should update parts' do
@parts[1].name = 'Very left column'
@page.send(:update_parts, @parts)
@page.parts.size.should == 3
@page.parts[1].name.should == 'Very left column'
@page.parts[1].slug.should == 'left_sidebar'
end
it 'should disable parts' do
@parts = [@parts.shift, @parts.pop]
@page.send(:update_parts, @parts)
@page.parts.size.should == 3
@page.parts[1].name.should == 'Left column'
@page.parts[1].disabled.should be_true
end
it 'should enable parts previously disabled' do
parts_at_first = @parts.clone
@parts = [@parts.shift, @parts.pop]
@page.send(:update_parts, @parts)
@page.send(:update_parts, parts_at_first)
@page.parts.size.should == 3
@page.parts[1].name.should == 'Left column'
@page.parts[1].disabled.should be_true
end
end
describe 'acts as tree' do
before(:each) do
@ -125,22 +233,21 @@ describe Page do
describe 'acts as list' do
before(:each) do
@home = Factory(:page)
@child_1 = Factory(:page, :title => 'Subpage 1', :slug => 'foo', :parent => @home, :site => @home.site)
@child_2 = Factory(:page, :title => 'Subpage 2', :slug => 'bar', :parent => @home, :site => @home.site)
@child_3 = Factory(:page, :title => 'Subpage 3', :slug => 'acme', :parent => @home, :site => @home.site)
end
before(:each) do
@home = Factory(:page)
@child_1 = Factory(:page, :title => 'Subpage 1', :slug => 'foo', :parent => @home, :site => @home.site)
@child_2 = Factory(:page, :title => 'Subpage 2', :slug => 'bar', :parent => @home, :site => @home.site)
@child_3 = Factory(:page, :title => 'Subpage 3', :slug => 'acme', :parent => @home, :site => @home.site)
end
it 'should be at the bottom of the folder once created' do
[@child_1, @child_2, @child_3].each_with_index { |c, i| c.position.should == i + 1 }
end
it 'should be at the bottom of the folder once created' do
[@child_1, @child_2, @child_3].each_with_index { |c, i| c.position.should == i + 1 }
end
it 'should have its position updated if a sibling is removed' do
@child_2.destroy
[@child_1, @child_3.reload].each_with_index { |c, i| c.position.should == i + 1 }
end
it 'should have its position updated if a sibling is removed' do
@child_2.destroy
[@child_1, @child_3.reload].each_with_index { |c, i| c.position.should == i + 1 }
end
end
end
end

View File

@ -9,7 +9,7 @@ require 'rspec/rails'
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
Rspec.configure do |config|
config.mock_with :rspec
config.mock_with :mocha
config.before(:each) do
Mongoid.master.collections.each(&:drop)