now can add new sites / accounts + basic membership mechanism iimplemented + fix a lot of minor bugs + add more rspec tests

This commit is contained in:
dinedine 2010-05-11 00:39:52 +02:00
parent a0216dc75f
commit 9901f53e12
60 changed files with 1130 additions and 306 deletions

View File

@ -0,0 +1,24 @@
module Admin
class AccountsController < BaseController
sections 'settings'
def new
@account = Account.new(:email => params[:email])
end
def create
@account = Account.new(params[:account])
if @account.save
current_site.memberships.create(:account => @account)
flash_success!
redirect_to edit_admin_current_site_url
else
flash_error!
render :action => 'new'
end
end
end
end

View File

@ -1,39 +1,55 @@
class Admin::BaseController < ::ApplicationController module Admin
class BaseController < ::ApplicationController
include Locomotive::Routing::SiteDispatcher include Locomotive::Routing::SiteDispatcher
layout 'admin' layout 'admin'
before_filter :authenticate_account! before_filter :authenticate_account!
before_filter :require_site
before_filter :validate_site_membership
before_filter :require_site before_filter :set_locale
helper_method :sections helper_method :sections
protected protected
def flash_success! def flash_success!(options = {})
flash[:success] = translate_flash_msg(:successful) msg = translate_flash_msg(:successful)
end (options.has_key?(:now) && options[:now] ? flash.now : flash)[:success] = msg
end
def flash_error!
flash[:error] = translate_flash_msg(:failed) def flash_error!(options = { :now => true })
end msg = translate_flash_msg(:failed)
(options.has_key?(:now) && options[:now] ? flash.now : flash)[:error] = msg
def translate_flash_msg(kind)
t("#{kind.to_s}_#{action_name}", :scope => [:admin, controller_name.underscore.gsub('/', '.'), :messages])
end
def self.sections(main, sub = nil)
write_inheritable_attribute(:sections, { :main => main, :sub => sub })
end
def sections(key = nil)
if !key.nil? && key.to_sym == :sub
self.class.read_inheritable_attribute(:sections)[:sub] || self.controller_name.dasherize
else
self.class.read_inheritable_attribute(:sections)[:main]
end end
end
def translate_flash_msg(kind)
t("#{kind.to_s}_#{action_name}", :scope => [:admin, controller_name.underscore.gsub('/', '.'), :messages])
end
def self.sections(main, sub = nil)
before_filter do |c|
sub = sub.call(c) if sub.respond_to?(:call)
sections = { :main => main, :sub => sub }
c.instance_variable_set(:@admin_sections, sections)
end
end
def sections(key = nil)
if !key.nil? && key.to_sym == :sub
@admin_sections[:sub] || self.controller_name.dasherize
else
@admin_sections[:main]
end
end
def set_locale
I18n.locale = current_account.locale
end
end
end end

View File

@ -0,0 +1,33 @@
module Admin
class CurrentSitesController < BaseController
sections 'settings', 'site'
def edit
@site = current_site
end
def update
@site = current_site
if @site.update_attributes(params[:site])
flash_success!
redirect_to edit_admin_current_site_url(new_host_if_subdomain_changed)
else
flash_error!
render :action => :edit
end
end
protected
def new_host_if_subdomain_changed
host_from_site = "#{@site.subdomain}.#{Locomotive.config.default_domain}"
if request.host == host_from_site
{}
else
{ :host => "#{host_from_site}:#{request.port}" }
end
end
end
end

View File

@ -1,54 +1,56 @@
class Admin::LayoutsController < Admin::BaseController module Admin
class LayoutsController < BaseController
sections 'settings' sections 'settings'
def index def index
@layouts = current_site.layouts @layouts = current_site.layouts
end end
def new def new
@layout = current_site.layouts.build @layout = current_site.layouts.build
end
def edit
@layout = current_site.layouts.find(params[:id])
end
def create
@layout = current_site.layouts.build(params[:layout])
if @layout.save
flash_success!
redirect_to edit_admin_layout_url(@layout)
else
flash_error!
render :action => 'new'
end end
end
def update def edit
@layout = current_site.layouts.find(params[:id]) @layout = current_site.layouts.find(params[:id])
end
def create
@layout = current_site.layouts.build(params[:layout])
if @layout.save
flash_success!
redirect_to edit_admin_layout_url(@layout)
else
flash_error!
render :action => 'new'
end
end
def update
@layout = current_site.layouts.find(params[:id])
if @layout.update_attributes(params[:layout]) if @layout.update_attributes(params[:layout])
flash_success! flash_success!
redirect_to edit_admin_layout_url(@layout) redirect_to edit_admin_layout_url(@layout)
else else
flash_error! flash_error!
render :action => "edit" render :action => "edit"
end
end end
end
def destroy def destroy
@layout = current_site.layouts.find(params[:id]) @layout = current_site.layouts.find(params[:id])
begin begin
@layout.destroy @layout.destroy
flash_success! flash_success!
rescue Exception => e rescue Exception => e
flash[:error] = e.to_s flash[:error] = e.to_s
end
redirect_to admin_layouts_url
end end
redirect_to admin_layouts_url
end
end
end end

View File

@ -0,0 +1,39 @@
module Admin
class MembershipsController < BaseController
sections 'settings'
def new
@membership = current_site.memberships.build
end
def create
@membership = current_site.memberships.build(params[:membership])
case @membership.action_to_take
when :create_account
redirect_to new_admin_account_url(:email => @membership.email)
when :save_it
current_site.save
flash_success!
redirect_to edit_admin_site_url
when :error
flash_error! :now => true
render :action => 'new'
when :nothing
flash[:error] = translate_flash_msg(:already_saved)
redirect_to edit_admin_site_url
end
end
def destroy
current_site.memberships.find(params[:id]).destroy
current_site.save
flash_success!
redirect_to edit_admin_site_url
end
end
end

View File

@ -0,0 +1,21 @@
module Admin
class MyAccountsController < BaseController
sections 'settings', 'account'
def edit
@account = current_account
end
def update
@account = current_account
if @account.update_attributes(params[:account])
flash_success!
redirect_to edit_admin_my_account_url
else
render :action => :edit
end
end
end
end

View File

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

View File

@ -1,68 +1,70 @@
class Admin::PagesController < Admin::BaseController module Admin
class PagesController < BaseController
sections 'contents' sections 'contents'
def index def index
@pages = current_site.pages.roots @pages = current_site.pages.roots
end
def new
@page = current_site.pages.build
@page.parts << PagePart.build_body_part
end
def edit
@page = current_site.pages.find(params[:id])
end
def create
@page = current_site.pages.build(params[:page])
if @page.save
flash_success!
redirect_to edit_admin_page_url(@page)
else
flash_error!
render :action => 'new'
end end
end
def update def new
@page = current_site.pages.find(params[:id]) @page = current_site.pages.build
@page.parts << PagePart.build_body_part
end
def edit
@page = current_site.pages.find(params[:id])
end
def create
@page = current_site.pages.build(params[:page])
if @page.save
flash_success!
redirect_to edit_admin_page_url(@page)
else
flash_error!
render :action => 'new'
end
end
def update
@page = current_site.pages.find(params[:id])
if @page.update_attributes(params[:page]) if @page.update_attributes(params[:page])
flash_success! flash_success!
redirect_to edit_admin_page_url(@page) redirect_to edit_admin_page_url(@page)
else else
flash_error! flash_error!
render :action => "edit" render :action => "edit"
end
end end
end
def sort def sort
@page = current_site.pages.find(params[:id]) @page = current_site.pages.find(params[:id])
@page.sort_children!(params[:children]) @page.sort_children!(params[:children])
render :json => { :message => translate_flash_msg(:successful) } render :json => { :message => translate_flash_msg(:successful) }
end
def get_path
page = current_site.pages.build(:parent => current_site.pages.find(params[:parent_id]), :slug => params[:slug].slugify)
render :json => { :url => page.url, :slug => page.slug }
end
def destroy
@page = current_site.pages.find(params[:id])
begin
@page.destroy
flash_success!
rescue Exception => e
flash[:error] = e.to_s
end end
redirect_to admin_pages_url
end
def get_path
page = current_site.pages.build(:parent => current_site.pages.find(params[:parent_id]), :slug => params[:slug].slugify)
render :json => { :url => page.url, :slug => page.slug }
end
def destroy
@page = current_site.pages.find(params[:id])
begin
@page.destroy
flash_success!
rescue Exception => e
flash[:error] = e.to_s
end
redirect_to admin_pages_url
end
end
end end

View File

@ -1,9 +1,11 @@
class Admin::PasswordsController < Devise::PasswordsController module Admin
class PasswordsController < Devise::PasswordsController
include Locomotive::Routing::SiteDispatcher include Locomotive::Routing::SiteDispatcher
layout 'login' layout 'login'
before_filter :require_site before_filter :require_site
end
end end

View File

@ -1,15 +1,17 @@
class Admin::SessionsController < Devise::SessionsController module Admin
class SessionsController < Devise::SessionsController
include Locomotive::Routing::SiteDispatcher include Locomotive::Routing::SiteDispatcher
layout 'login' layout 'login'
before_filter :require_site before_filter :require_site
protected protected
def after_sign_in_path_for(resource)
admin_pages_url
end
def after_sign_in_path_for(resource)
admin_pages_url
end end
end end

View File

@ -0,0 +1,48 @@
module Admin
class SitesController < BaseController
sections 'settings'
def new
@site = Site.new
end
def create
@site = Site.new(params[:site])
if @site.save
@site.memberships.create :account => @current_account, :admin => true
flash_success!
redirect_to edit_admin_my_account_url
else
flash_error!
render :action => 'new'
end
end
def destroy
@site = current_account.sites.detect { |s| s._id == params[:id] }
if @site != current_site
@site.destroy
flash_success!
else
flash_error!
end
redirect_to edit_admin_my_account_url
end
protected
def new_host_if_subdomain_changed
host_from_site = "#{@site.subdomain}.#{Locomotive.config.default_domain}"
if request.host == host_from_site
{}
else
{ :host => "#{host_from_site}:#{request.port}" }
end
end
end
end

View File

@ -1,54 +1,56 @@
class Admin::SnippetsController < Admin::BaseController module Admin
class SnippetsController < BaseController
sections 'settings' sections 'settings'
def index def index
@snippets = current_site.snippets @snippets = current_site.snippets
end
def new
@snippet = current_site.snippets.build
end
def edit
@snippet = current_site.snippets.find(params[:id])
end
def create
@snippet = current_site.snippets.build(params[:snippet])
if @snippet.save
flash_success!
redirect_to edit_admin_snippet_url(@snippet)
else
flash_error!
render :action => 'new'
end end
end
def update def new
@snippet = current_site.snippets.find(params[:id]) @snippet = current_site.snippets.build
end
def edit
@snippet = current_site.snippets.find(params[:id])
end
def create
@snippet = current_site.snippets.build(params[:snippet])
if @snippet.save
flash_success!
redirect_to edit_admin_snippet_url(@snippet)
else
flash_error!
render :action => 'new'
end
end
def update
@snippet = current_site.snippets.find(params[:id])
if @snippet.update_attributes(params[:snippet]) if @snippet.update_attributes(params[:snippet])
flash_success! flash_success!
redirect_to edit_admin_snippet_url(@snippet) redirect_to edit_admin_snippet_url(@snippet)
else else
flash_error! flash_error!
render :action => "edit" render :action => "edit"
end
end end
end
def destroy def destroy
@snippet = current_site.snippets.find(params[:id]) @snippet = current_site.snippets.find(params[:id])
begin begin
@snippet.destroy @snippet.destroy
flash_success! flash_success!
rescue Exception => e rescue Exception => e
flash[:error] = e.to_s flash[:error] = e.to_s
end
redirect_to admin_snippets_url
end end
redirect_to admin_snippets_url
end
end
end end

View File

@ -0,0 +1,8 @@
module Admin::AccountsHelper
def admin_on?(site = current_site)
site.memberships.detect { |a| a.admin? && a.account == current_account }
end
end

View File

@ -0,0 +1,24 @@
module Admin::SitesHelper
def application_domain
domain = Locomotive.config.default_domain
domain += ":#{request.port}" if request.port != 80
domain
end
def main_site_url(site = current_site, options = {})
url = "http://#{site.subdomain}.#{Locomotive.config.default_domain}"
url += ":#{request.port}" if request.port != 80
url = File.join(url, controller.request.fullpath) if options.has_key?(:uri) && options[:uri]
url
end
def error_on_domain(site, name)
if (error = (site.errors[:domains] || []).detect { |n| n.include?(name) })
content_tag(:span, error, :class => 'inline-errors')
else
''
end
end
end

View File

@ -3,7 +3,7 @@ class Account
include Mongoid::Timestamps include Mongoid::Timestamps
# devise modules # devise modules
devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable #:registerable, devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable
# attr_accessible :email, :password, :password_confirmation # TODO # attr_accessible :email, :password, :password_confirmation # TODO
@ -16,8 +16,31 @@ class Account
## associations ## ## associations ##
## callbacks ##
before_destroy :remove_memberships!
## methods ##
def sites def sites
Site.where({ :account_ids => self._id }) Site.where({ 'memberships.account_id' => self._id })
end
protected
def password_required?
!persisted? || !password.blank? || !password_confirmation.blank?
end
def remove_memberships!
self.sites.each do |site|
site.memberships.delete_if { |m| m.account_id == self._id }
if site.admin_memberships.empty?
raise I18n.t('errors.messages.needs_admin_account')
else
site.save
end
end
end end
end end

View File

@ -28,7 +28,7 @@ class Layout < LiquidTemplate
slug = part[0].strip.downcase slug = part[0].strip.downcase
if slug == 'layout' if slug == 'layout'
body = PagePart.new :slug => slug, :name => I18n.t('admin.shared.attributes.body') body = PagePart.new :slug => slug, :name => I18n.t('attributes.defaults.page_parts.name')
else else
self.parts.build :slug => slug, :name => (part[1] || slug).gsub("\"", '') self.parts.build :slug => slug, :name => (part[1] || slug).gsub("\"", '')
end end

36
app/models/membership.rb Normal file
View File

@ -0,0 +1,36 @@
class Membership
include Mongoid::Document
include Mongoid::Timestamps
## fields ##
field :admin, :type => Boolean, :default => false
## associations ##
belongs_to_related :account
embedded_in :site, :inverse_of => :memberships
## validations ##
validates_presence_of :account
## methods ##
def email; @email; end
def email=(email)
@email = email
self.account = Account.where(:email => email).first
end
def action_to_take
if @email.blank?
:error
elsif self.account.nil?
:create_account
elsif self.site.memberships.find_all { |m| m.account_id == self.account_id }.size > 1
:nothing
else
:save_it
end
end
end

View File

@ -21,21 +21,21 @@ class Page
before_validate :normalize_slug before_validate :normalize_slug
before_save { |p| p.parent_id = nil if p.parent_id.blank? } before_save { |p| p.parent_id = nil if p.parent_id.blank? }
before_save :change_parent before_save :change_parent
before_create { |p| p.parts << PagePart.build_body_part }
before_create { |p| p.fix_position(false) } before_create { |p| p.fix_position(false) }
before_create :add_to_list_bottom before_create :add_to_list_bottom
# before_create :add_body_part before_destroy :do_not_remove_index_and_404_pages
before_destroy :remove_from_list before_destroy :remove_from_list
## validations ## ## validations ##
validates_presence_of :site, :title, :slug validates_presence_of :site, :title, :slug
validates_uniqueness_of :slug, :scope => :site_id validates_uniqueness_of :slug, :scope => [:site_id, :parent_id]
validates_exclusion_of :slug, :in => Locomotive.config.reserved_slugs, :if => Proc.new { |p| p.depth == 0 } validates_exclusion_of :slug, :in => Locomotive.config.reserved_slugs, :if => Proc.new { |p| p.depth == 0 }
## named scopes ## ## named scopes ##
## behaviours ## ## behaviours ##
acts_as_tree :order => ['position', 'asc'] acts_as_tree :order => ['position', 'asc']
# accepts_nested_attributes_for :parts, :allow_destroy => true
## methods ## ## methods ##
@ -50,10 +50,6 @@ class Page
def parts_attributes=(attributes) def parts_attributes=(attributes)
self.update_parts(attributes.values.map { |attrs| PagePart.new(attrs) }) self.update_parts(attributes.values.map { |attrs| PagePart.new(attrs) })
end end
def add_body_part
self.parts.build :name => 'body', :slug => 'layout', :value => '---body here---'
end
def parent=(owner) # missing in acts_as_tree def parent=(owner) # missing in acts_as_tree
@_parent = owner @_parent = owner
@ -87,6 +83,17 @@ class Page
protected protected
def do_not_remove_index_and_404_pages
# safe_site = self.site rescue nil
# return if safe_site.nil?
return if (self.site rescue nil).nil?
if self.index? || self.not_found?
raise I18n.t('errors.messages.protected_page')
end
end
def update_parts(parts) def update_parts(parts)
performed = [] performed = []
@ -143,6 +150,8 @@ class Page
end end
def remove_from_list def remove_from_list
return if (self.site rescue nil).nil?
Page.where(:parent_id => self.parent_id).and(:position.gt => self.position).each do |p| Page.where(:parent_id => self.parent_id).and(:position.gt => self.position).each do |p|
p.position -= 1 p.position -= 1
p.save p.save

View File

@ -1,6 +1,5 @@
class PagePart class PagePart
include Mongoid::Document include Mongoid::Document
# include Mongoid::Timestamps
## fields ## ## fields ##
field :name, :type => String field :name, :type => String
@ -12,8 +11,6 @@ class PagePart
## associations ## ## associations ##
embedded_in :page, :inverse_of => :parts embedded_in :page, :inverse_of => :parts
# attr_accessor :_delete
## callbacks ## ## 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? }
@ -25,12 +22,11 @@ class PagePart
## methods ## ## 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 def self.build_body_part
self.new(:name => I18n.t('admin.shared.attributes.body'), :slug => 'layout') self.new({
:name => I18n.t('attributes.defaults.page_parts.name'),
:value => I18n.t('attributes.defaults.pages.other.body'),
:slug => 'layout'
})
end end
end end

View File

@ -6,12 +6,12 @@ class Site
field :name field :name
field :subdomain, :type => String field :subdomain, :type => String
field :domains, :type => Array, :default => [] field :domains, :type => Array, :default => []
field :account_ids, :type => Array, :default => []
## associations ## ## associations ##
has_many_related :pages has_many_related :pages
has_many_related :layouts has_many_related :layouts
has_many_related :snippets has_many_related :snippets
embeds_many :memberships
## validations ## ## validations ##
validates_presence_of :name, :subdomain validates_presence_of :name, :subdomain
@ -21,23 +21,24 @@ class Site
validate :domains_must_be_valid_and_unique validate :domains_must_be_valid_and_unique
## callbacks ## ## callbacks ##
after_create :create_default_pages!
before_save :add_subdomain_to_domains before_save :add_subdomain_to_domains
after_destroy :destroy_in_cascade!
## named scopes ## ## named scopes ##
named_scope :match_domain, lambda { |domain| { :where => { :domains => domain } } } named_scope :match_domain, lambda { |domain| { :where => { :domains => domain } } }
named_scope :match_domain_with_exclusion_of, lambda { |domain, site| { :where => { :domains => domain, :id.ne => site.id } } } named_scope :match_domain_with_exclusion_of, lambda { |domain, site| { :where => { :domains => domain, :_id.ne => site.id } } }
## behaviours ## ## behaviours ##
add_dirty_methods :domains
## methods ## ## methods ##
def accounts def accounts
Account.criteria.in(:_id => self.account_ids) Account.criteria.in(:_id => self.memberships.collect(&:account_id))
end end
def accounts=(models_or_ids) def admin_memberships
self.account_ids = [*models_or_ids].collect { |object| object.respond_to?(:to_i) ? object : object.id }.uniq self.memberships.find_all { |m| m.admin? }
end end
def add_subdomain_to_domains def add_subdomain_to_domains
@ -49,13 +50,17 @@ class Site
(self.domains || []) - ["#{self.subdomain}.#{Locomotive.config.default_domain}"] (self.domains || []) - ["#{self.subdomain}.#{Locomotive.config.default_domain}"]
end end
def domains_with_subdomain
((self.domains || []) + ["#{self.subdomain}.#{Locomotive.config.default_domain}"]).uniq
end
protected protected
def domains_must_be_valid_and_unique def domains_must_be_valid_and_unique
return if self.domains.empty? || (!self.new_record? && !self.domains_changed?) return if self.domains.empty?
(self.domains_without_subdomain - (self.domains_was || [])) .each do |domain| self.domains_without_subdomain.each do |domain|
if not self.class.match_domain_with_exclusion_of(domain, self).first.nil? if not self.class.match_domain_with_exclusion_of(domain, self).empty?
self.errors.add(:domains, :domain_taken, :value => domain) self.errors.add(:domains, :domain_taken, :value => domain)
end end
@ -65,4 +70,20 @@ class Site
end end
end end
def create_default_pages!
%w{index 404}.each do |slug|
self.pages.create({
:slug => slug,
:title => I18n.t("attributes.defaults.pages.#{slug}.title"),
:body => I18n.t("attributes.defaults.pages.#{slug}.body")
})
end
end
def destroy_in_cascade!
%w{pages layouts snippets}.each do |association|
self.send(association).destroy_all
end
end
end end

View File

@ -0,0 +1,22 @@
- title t('.title')
- content_for :submenu do
= render 'admin/shared/menu/settings'
%p= t('.help')
= semantic_form_for @account, :url => admin_accounts_url do |f|
= f.foldable_inputs :name => :information do
= f.input :name, :required => false
= f.foldable_inputs :name => :credentials do
= f.input :email, :required => false
= f.custom_input :password, :label => :new_password do
= f.password_field :password
= f.custom_input :password_confirmation, :label => :new_password_confirmation do
= f.password_field :password_confirmation
= render 'admin/shared/form_actions', :back_url => edit_admin_current_site_url, :button_label => :create

View File

@ -0,0 +1,45 @@
- content_for :head do
= javascript_include_tag 'admin/site'
= f.foldable_inputs :name => :information, :style => "#{'display: none' unless @site.new_record?}" do
= f.input :name, :required => false
= f.foldable_inputs :name => :access_points, :class => 'editable-list off' do
= f.custom_input :subdomain, :css => 'path' do
%em
http://
= f.text_field :subdomain
\.
%em
= application_domain
- @site.domains_without_subdomain.each_with_index do |name, index|
%li{ :class => "item added #{'last' if index == @site.domains.size - 1}"}
%em
http://
= text_field_tag 'site[domains][]', name
&nbsp;
= error_on_domain(@site, name)
%span.actions
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm')
%li.item.new
%em
http://
= text_field_tag 'label', t('formtastic.hints.site.domain_name'), :class => 'string label void'
&nbsp;
%span.actions
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm')
%button{ :class => 'button light add', :type => 'button' }
%span= t('admin.buttons.new_item')
= f.foldable_inputs :name => :memberships, :class => 'memberships' do
- @site.memberships.each_with_index do |membership, index|
- account = membership.account
%li{ :class => "item #{'last' if index == @site.memberships.size - 1}" }
%strong= account.name
%em= account.email
- if account != current_account
%span.actions
= link_to image_tag('admin/form/icons/trash.png'), admin_membership_url(membership), :class => 'remove first', :confirm => t('admin.messages.confirm'), :method => :delete

View File

@ -0,0 +1,15 @@
- title link_to(@site.name.blank? ? @site.name_was : @site.name, '#', :rel => 'site_name', :title => t('.ask_for_name'), :class => 'editable')
- content_for :submenu do
= render 'admin/shared/menu/settings'
- content_for :buttons do
= admin_button_tag t('.new_membership'), new_admin_membership_url, :class => 'add'
%p= t('.help')
= semantic_form_for @site, :url => admin_current_site_url do |f|
= render 'form', :f => f
= render 'admin/shared/form_actions', :button_label => :update

View File

@ -0,0 +1,15 @@
- title t('.title')
- content_for :submenu do
= render 'admin/shared/menu/settings'
%p= t('.help')
= semantic_form_for @membership, :url => admin_memberships_url do |f|
= f.inputs :name => :membership_email, :class => 'inputs email' do
= f.custom_input :email, { :css => 'string full', :with_label => false } do
= f.text_field :email
= render 'admin/shared/form_actions', :back_url => edit_admin_current_site_url, :button_label => :create

View File

@ -0,0 +1,42 @@
- title link_to(@account.name.blank? ? @account.name_was : @account.name, '#', :rel => 'account_name', :title => t('.ask_for_name'), :class => 'editable')
- content_for :submenu do
= render 'admin/shared/menu/settings'
- content_for :buttons do
= admin_button_tag t('.new_site'), new_admin_site_url, :class => 'add'
%p= t('.help')
= semantic_form_for @account, :url => admin_my_account_url do |f|
= f.foldable_inputs :name => :information, :style => 'display: none' do
= f.input :name
= f.foldable_inputs :name => :credentials do
= f.input :email
= f.custom_input :password, :label => :new_password do
= f.password_field :password
= f.custom_input :password_confirmation, :label => :new_password_confirmation do
= f.password_field :password_confirmation
= f.foldable_inputs :name => :sites, :class => 'sites off' do
- @account.sites.each do |site|
%li{ :class => 'item' }
%strong= link_to site.name, main_site_url(site, :uri => true)
%em= site.domains.join(', ')
- if admin_on?(site) && site != current_site
%span{ :class => 'actions' }
= link_to image_tag('admin/form/icons/trash.png'), admin_site_url(site), :class => 'remove first', :confirm => t('admin.messages.confirm'), :method => :delete
= f.foldable_inputs :name => :language, :class => 'language' do
= f.custom_input :language, { :css => 'full', :with_label => false } do
- Locomotive.config.locales.each do |locale|
%span
= image_tag "admin/flags/#{locale}.png"
= f.radio_button :locale, locale
&nbsp;
= t(".#{locale}")
= render 'admin/shared/form_actions', :button_label => :update

View File

@ -15,4 +15,4 @@
= link_to t('.link'), new_account_session_path(resource_name) = link_to t('.link'), new_account_session_path(resource_name)
.footer .footer
= submit_button_tag t('buttons.change_password') = submit_button_tag t('admin.buttons.change_password')

View File

@ -14,4 +14,4 @@
= link_to t('.link'), new_account_session_path(resource_name) = link_to t('.link'), new_account_session_path(resource_name)
.footer .footer
= submit_button_tag t('buttons.send_password') = submit_button_tag t('admin.buttons.send_password')

View File

@ -12,4 +12,4 @@
= link_to t('.link'), new_password_path(resource_name) = link_to t('.link'), new_password_path(resource_name)
.footer .footer
= submit_button_tag t('buttons.login') = submit_button_tag t('admin.buttons.login')

View File

@ -1,7 +1,10 @@
.actions .actions
.span-12 .span-12
%p %p
= link_to escape_once('&larr;&nbsp;') + t('.back'), back_url - if defined?(back_url)
= link_to escape_once('&larr;&nbsp;') + t('.back'), back_url
- else
&nbsp;
.span-12.last .span-12.last
%p %p

View File

@ -1,8 +1,8 @@
%h1= link_to current_site.name, '#' %h1= link_to current_site.name, '#'
#global-actions-bar #global-actions-bar
= t('.welcome', :name => link_to(current_account.name, '#')) = t('.welcome', :name => link_to(current_account.name, edit_admin_my_account_url))
%span= '|' %span= '|'
= link_to t('.see'), '#' = link_to t('.see'), main_site_url
%span= '|' %span= '|'
= link_to t('.logout'), destroy_account_session_url, :confirm => t('admin.messages.confirm') = link_to t('.logout'), destroy_account_session_url, :confirm => t('admin.messages.confirm')

View File

@ -1,5 +1,5 @@
%ul#menu %ul#menu
= admin_menu_item('contents', admin_pages_url) = admin_menu_item('contents', admin_pages_url)
= admin_menu_item('assets', '#') = admin_menu_item('assets', '#')
= admin_menu_item('settings', admin_layouts_url) = admin_menu_item('settings', edit_admin_current_site_url)
%li.clear %li.clear

View File

@ -1,3 +1,5 @@
%ul %ul
= admin_submenu_item 'site', edit_admin_current_site_url
= admin_submenu_item 'layouts', admin_layouts_url = admin_submenu_item 'layouts', admin_layouts_url
= admin_submenu_item 'snippets', admin_snippets_url = admin_submenu_item 'snippets', admin_snippets_url
= admin_submenu_item 'account', edit_admin_my_account_url

View File

@ -0,0 +1,35 @@
- content_for :head do
= javascript_include_tag 'admin/site'
= f.foldable_inputs :name => :information, :style => "#{'display: none' unless @site.new_record?}" do
= f.input :name, :required => false
= f.foldable_inputs :name => :access_points, :class => 'editable-list off' do
= f.custom_input :subdomain, :css => 'path' do
%em
http://
= f.text_field :subdomain
\.
%em
= application_domain
- @site.domains_without_subdomain.each_with_index do |name, index|
%li{ :class => "item added #{'last' if index == @site.domains.size - 1}"}
%em
http://
= text_field_tag 'site[domains][]', name
&nbsp;
= error_on_domain(@site, name)
%span.actions
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm')
%li.item.new
%em
http://
= text_field_tag 'label', t('formtastic.hints.site.domain_name'), :class => 'string label void'
&nbsp;
%span.actions
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm')
%button{ :class => 'button light add', :type => 'button' }
%span= t('admin.buttons.new_item')

View File

@ -0,0 +1,12 @@
- title t('.title')
- content_for :submenu do
= render 'admin/shared/menu/settings'
%p= t('.help')
= semantic_form_for @site, :url => admin_sites_url do |form|
= render 'form', :f => form
= render 'admin/shared/form_actions', :back_url => edit_admin_my_account_url, :button_label => :create

View File

@ -43,6 +43,6 @@ module Locomotive
# Configure sensitive parameters which will be filtered from the log file. # Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters << :password config.filter_parameters << :password
config.secret_token = '968a457262807c64e3ed5609882e17a774b917f5bcf2d308bd37eac4ba4d416d5692e6b13d77523fddb94c1dd603f160db8492b86b5e0203240bf339fe2aeae4' config.secret_token = '968a457262807c64e3ed5609882e17a774b917f5bcf2d308bd37eac4ba4d416d5692e6b13d77523fddb94c1dd603f160db8492b86b5e0203240bf339fe2aeae4'
end end
end end

View File

@ -5,5 +5,10 @@ Locomotive.configure do |config|
config.default_domain = 'example.com' config.default_domain = 'example.com'
end end
# TODO: embed it in Locomotive right after configure # TODO: embed them in Locomotive right after configure
ActionMailer::Base.default_url_options[:host] = Locomotive.config.default_domain + (Rails.env.development? ? ':3000' : '') ActionMailer::Base.default_url_options[:host] = Locomotive.config.default_domain + (Rails.env.development? ? ':3000' : '')
Rails.application.config.session_store :cookie_store, {
:key => '_locomotive_session',
:domain => ".#{Locomotive.config.default_domain}"
}

View File

@ -20,12 +20,30 @@ module Mongoid #:nodoc:
# Enabling scope in validates_uniqueness_of validation # Enabling scope in validates_uniqueness_of validation
module Validations #:nodoc: module Validations #:nodoc:
class UniquenessValidator < ActiveModel::EachValidator class UniquenessValidator < ActiveModel::EachValidator
def validate_each(document, attribute, value, scope = nil) def validate_each(document, attribute, value)
criteria = { attribute => value, :_id.ne => document._id } conditions = { attribute => value, :_id.ne => document._id }
criteria[scope] = document.send(scope) if scope
return if document.class.where(criteria).empty? if options.has_key?(:scope) && !options[:scope].nil?
document.errors.add(attribute, :taken, :default => options[:message], :value => value) [*options[:scope]].each do |scoped_attr|
conditions[scoped_attr] = document.attributes[scoped_attr]
end
end
# Rails.logger.debug "conditions = #{conditions.inspect} / #{options[:scope].inspect}"
return if document.class.where(conditions).empty?
# if document.new_record? || key_changed?(document)
document.errors.add(attribute, :taken, :default => options[:message], :value => value)
# end
end end
# protected
# def key_changed?(document)
# (document.primary_key || {}).each do |key|
# return true if document.send("#{key}_changed?")
# end; false
# end
end end
end end

View File

@ -1,3 +0,0 @@
Rails.application.config.session_store :cookie_store, {
:key => '_locomotive_session',
}

View File

@ -1,5 +1,36 @@
en: en:
admin: admin:
buttons:
login: Log in
send_password: Send
change_password: Update
new_item: "+ add"
messages:
confirm: Are you sure ?
shared:
header:
welcome: Welcome, {{name}}
see: See website
logout: Log out
menu:
contents: Contents
assets: Assets
settings: Settings
pages: Pages
layouts: Layouts
snippets: Snippets
account: My account
site: Site
footer:
developed_by: Developed by
powered_by: and Powered by
form_actions:
back: Back without saving
create: Create
update: Update
sessions: sessions:
new: new:
title: Login title: Login
@ -16,38 +47,17 @@ en:
title: Update my password title: Update my password
link: "&rarr; Back to login page" link: "&rarr; Back to login page"
password: "Your new password" password: "Your new password"
password_confirmation: "Confirmation of your new password" password_confirmation: "Confirmation of your new password"
messages:
confirm: Are you sure ?
shared:
header:
welcome: Welcome, {{name}}
see: See website
logout: Log out
menu:
contents: Contents
assets: Assets
settings: Settings
pages: Pages
layouts: Layouts
snippets: Snippets
footer:
developed_by: Developed by
powered_by: and Powered by
form_actions:
back: Back without saving
create: Create
update: Update
attributes:
body: Body
pages: pages:
index: index:
title: Listing pages title: Listing pages
no_items: "There are no pages for now. Just click <a href=\"{{url}}\">here</a> to create the first one." no_items: "There are no pages for now. Just click <a href=\"{{url}}\">here</a> to create the first one."
new: new page new: new page
page:
updated_at: updated at
edit:
show: show
messages: messages:
successful_create: "Page was successfully created." successful_create: "Page was successfully created."
successful_update: "Page was successfully updated." successful_update: "Page was successfully updated."
@ -64,20 +74,47 @@ en:
no_items: "There are no snippets for now. Just click <a href=\"{{url}}\">here</a> to create the first one." no_items: "There are no snippets for now. Just click <a href=\"{{url}}\">here</a> to create the first one."
new: new snippet new: new snippet
sites:
new:
title: New site
current_sites:
edit:
new_membership: add account
memberships:
new:
title: New membership
help: "Please give the account email to add. If it does not exist, you will be redirected to the account creation form."
accounts:
new:
title: New account
my_accounts:
edit:
new_site: new site
en: English
fr: French
formtastic: formtastic:
titles: titles:
information: General information information: General information
meta: Meta meta: Meta
code: Code code: Code
credentials: Credentials
language: Language
sites: Sites
access_points: Access points
memberships: Accounts
membership_email: Account email
hints: hints:
page: page:
keywords: "Meta keywords used within the head tag of the page. They are separeted by an empty space. Required for SEO." keywords: "Meta keywords used within the head tag of the page. They are separeted by an empty space. Required for SEO."
description: "Meta description used within the head tag of the page. Required for SEO." description: "Meta description used within the head tag of the page. Required for SEO."
snippet: snippet:
slug: "You need to know it in order to insert the snippet inside a page or a layout" slug: "You need to know it in order to insert the snippet inside a page or a layout"
site:
domain_name: "ex: locomotiveapp.org"
buttons:
login: Log in
send_password: Send
change_password: Update

View File

@ -3,4 +3,20 @@ en:
messages: messages:
domain_taken: "{{value}} is already taken" domain_taken: "{{value}} is already taken"
invalid_domain: "{{value}} is invalid" invalid_domain: "{{value}} is invalid"
missing_content_for_layout: "should contain 'content_for_layout' liquid tag" missing_content_for_layout: "should contain 'content_for_layout' liquid tag"
needs_admin_account: "One admin account is required at least"
protected_page: "You can not remove index or 404 pages"
attributes:
defaults:
pages:
index:
title: "Home page"
body: "Content of the home page"
"404":
title: "Page not found"
body: "Content of the 404 page"
other:
body: "Content goes here"
page_parts:
name: "Body"

View File

@ -27,11 +27,17 @@ Locomotive::Application.routes.draw do |map|
end end
resources :snippets resources :snippets
# get 'login' => 'sessions#new', :as => :new_account_session resources :site
# post 'login' => 'sessions#create', :as => :account_session
# get 'logout' => 'sessions#destroy', :as => :destroy_account_session resource :current_site
# resource :password, :only => [:new, :create, :edit, :update], :controller => 'devise/passwords'
resources :accounts
resource :my_account
resources :memberships
end end
# magic url
match '/' => 'pages#show' match '/' => 'pages#show'
end end

View File

@ -1,2 +1,5 @@
Site.create! :name => 'Locomotive test website', :subdomain => 'test' account = Account.create! :name => 'Admin', :email => 'admin@locomotiveapp.org', :password => 'locomotive', :password_confirmation => 'locomotive'
Account.create :name => 'Admin', :email => 'admin@locomotiveapp.org', :password => 'locomotive', :password_confirmation => 'locomotive'
site = Site.new :name => 'Locomotive test website', :subdomain => 'test'
site.memberships.build :account => account, :admin => true
site.save!

View File

@ -1,6 +1,3 @@
- scoping
- devise messages in French
- localize devise emails
x admin layout x admin layout
x logout button x logout button
x slugify page x slugify page
@ -22,12 +19,31 @@ x parts js/css:
x page parts x page parts
x layout part should be always in first x layout part should be always in first
x pages section (CRUD) x pages section (CRUD)
- slug unique within a folder x my account section (part of settings)
- validates_uniqueness_of :slug, :scope => :id x add new accounts
- refactoring page.rb => create module pagetree x edit site settings
- my account section (part of settings) x slug unique within a folder
- layouts section x layouts section
- create 404 + index pages once a site is created x create new site
- can not delete index + 404 pages x share session accross domains (only subdomains)
x destroy site
x remove all pages, snippets, ...etc when destroying a website
x destroy account
x can not delete the only one admin account for a site
x create 404 + index pages once a site is created
x can not delete index + 404 pages
x validates_uniqueness_of :slug, :scope => :id
x domain scoping when authenticating
BACKLOG:
- liquid rendering engine
- theme assets
- assets collection
- devise messages in French
- localize devise emails
- refactoring admin crud (pages + layouts + snippets) - refactoring admin crud (pages + layouts + snippets)
- remove all pages, snippets, ...etc when destroying a website - refactoring page.rb => create module pagetree

View File

@ -7,7 +7,8 @@ module Locomotive
:default_domain => 'rails.local.fr', :default_domain => 'rails.local.fr',
:reserved_subdomains => %w{www admin email blog webmail mail support help site sites}, :reserved_subdomains => %w{www admin email blog webmail mail support help site sites},
# :forbidden_paths => %w{layouts snippets stylesheets javascripts assets admin system api}, # :forbidden_paths => %w{layouts snippets stylesheets javascripts assets admin system api},
:reserved_slugs => %w{stylesheets javascripts assets admin images api pages} :reserved_slugs => %w{stylesheets javascripts assets admin images api pages},
:locales => %w{en fr}
} }
cattr_accessor :settings cattr_accessor :settings

View File

@ -19,17 +19,22 @@ module Locomotive
protected protected
def fetch_site def fetch_site
@site = Site.match_domain(request.host).first @current_site ||= Site.match_domain(request.host).first
end end
def current_site def current_site
@site ||= fetch_site @current_site || fetch_site
end end
def require_site def require_site
redirect_to application_root_url and return false if current_site.nil? redirect_to application_root_url and return false if current_site.nil?
end end
def validate_site_membership
return if current_site && current_site.accounts.include?(current_account)
redirect_to application_root_url
end
def application_root_url def application_root_url
root_url(:host => Locomotive.config.default_domain, :port => request.port) root_url(:host => Locomotive.config.default_domain, :port => request.port)
end end

View File

@ -23,4 +23,13 @@ class MiscFormBuilder < Formtastic::SemanticFormBuilder
template.content_tag(:li, template.find_and_preserve(html), :class => "#{options[:css]} #{'error' unless @object.errors[name].empty?}") template.content_tag(:li, template.find_and_preserve(html), :class => "#{options[:css]} #{'error' unless @object.errors[name].empty?}")
end end
def inline_errors_on(method, options = nil)
if render_inline_errors?
errors = @object.errors[method.to_sym]
template.content_tag(:span, [*errors].to_sentence.untaint, :class => 'inline-errors') if errors.present?
else
nil
end
end
end end

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

View File

@ -0,0 +1,36 @@
$(document).ready(function() {
var defaultValue = $('fieldset.editable-list li.new input[type=text]').val();
/* __ fields ___ */
$('fieldset.editable-list li.new input[type=text]').focus(function() {
if ($(this).hasClass('void') && $(this).parents('li').hasClass('new'))
$(this).val('').removeClass('void');
});
$('fieldset.editable-list li.new button').click(function() {
var lastRow = $(this).parents('li.new');
var currentValue = lastRow.find('input.label').val();
if (currentValue == defaultValue || currentValue == '') return;
var newRow = lastRow.clone(true).removeClass('new').addClass('added').insertBefore(lastRow);
// should copy the value of the select box
var input = newRow.find('input.label')
.attr('name', 'site[domains][]');
if (lastRow.find('input.label').val() == '') input.val("undefined");
// then reset the form
lastRow.find('input').val(defaultValue).addClass('void');
lastRow.find('select').val('input');
});
$('fieldset.editable-list li a.remove').click(function(e) {
if (confirm($(this).attr('data-confirm')))
$(this).parents('li').remove();
e.preventDefault();
e.stopPropagation();
});
});

View File

@ -217,7 +217,7 @@ form.formtastic fieldset ol li.item span.actions {
/* ___ editable-list (content type fields and validations) ___ */ /* ___ editable-list (content type fields and validations) ___ */
form.formtastic fieldset.editable-list ol { padding-left: 20px; } form.formtastic fieldset.editable-list ol { padding-left: 20px; padding-right: 20px; width: 880px; }
form.formtastic fieldset.editable-list ol li { margin-left: 0px !important; } form.formtastic fieldset.editable-list ol li { margin-left: 0px !important; }
@ -282,6 +282,15 @@ form.formtastic fieldset.editable-list ol li.added input:focus {
border: 1px solid #a6a8b8; border: 1px solid #a6a8b8;
} }
form.formtastic fieldset.editable-list ol li.added .inline-errors {
position: relative;
top: -1px;
padding: 2px 3px;
background: #FFE5E5;
color: #CE2525;
font-size: 0.8em;
}
form.formtastic fieldset.editable-list ol li.new { form.formtastic fieldset.editable-list ol li.new {
height: 42px; height: 42px;
background-image: url(/images/admin/form/big_item.png); background-image: url(/images/admin/form/big_item.png);

View File

@ -55,6 +55,6 @@
#page-parts code textarea { #page-parts code textarea {
width: 880px; width: 880px;
height: 400px; height: 400px;
background: transparent url(../../images/admin/form/field.png) repeat-x 0 0; background: transparent url(/images/admin/form/field.png) repeat-x 0 0;
border: 0px; border: 0px;
} }

View File

@ -4,7 +4,7 @@
font-family: monospace; font-family: monospace;
font-size: 10pt; font-size: 10pt;
color: black; color: black;
background: white url(../../images/admin/form/field.png) repeat-x 0 0 !important; background: white url(/images/admin/form/field.png) repeat-x 0 0 !important;
} }
pre.code, .editbox { pre.code, .editbox {

View File

@ -4,7 +4,7 @@
font-family: monospace; font-family: monospace;
font-size: 10pt; font-size: 10pt;
color: black; color: black;
background: white url(../../images/admin/form/field.png) repeat-x 0 0 !important; background: white url(/images/admin/form/field.png) repeat-x 0 0 !important;
} }
pre.code, .editbox { pre.code, .editbox {

View File

@ -4,7 +4,7 @@
font-family: monospace; font-family: monospace;
font-size: 10pt; font-size: 10pt;
color: black; color: black;
background: white url(../../images/admin/form/field.png) repeat-x 0 0 !important; background: white url(/images/admin/form/field.png) repeat-x 0 0 !important;
} }
.editbox p { .editbox p {

View File

@ -14,6 +14,12 @@ Factory.define :account do |a|
a.locale 'en' a.locale 'en'
end end
## Memberships ##
Factory.define :membership do |m|
m.association :account, :factory => :account
m.admin true
end
## Pages ## ## Pages ##
Factory.define :page do |p| Factory.define :page do |p|
p.association :site, :factory => :site p.association :site, :factory => :site

View File

@ -17,8 +17,7 @@ describe Account do
end end
it "should have a default locale" do it "should have a default locale" do
account = Factory.build(:account, :locale => nil) account = Account.new
account.should be_valid
account.locale.should == 'en' account.locale.should == 'en'
end end
@ -32,9 +31,35 @@ describe Account do
it 'should own many sites' do it 'should own many sites' do
account = Factory(:account) account = Factory(:account)
site_1 = Factory(:site, :accounts => account) site_1 = Factory(:site, :memberships => [Membership.new(:account => account)])
site_2 = Factory(:site, :subdomain => 'foo', :accounts => account) site_2 = Factory(:site, :subdomain => 'foo', :memberships => [Membership.new(:account => account)])
account.sites.should == [site_1, site_2] account.sites.should == [site_1, site_2]
end end
describe 'deleting' do
before(:each) do
@account = Factory.build(:account)
@site_1 = Factory.build(:site, :subdomain => 'foo', :memberships => [Factory.build(:membership, :account => @account)])
@site_2 = Factory.build(:site, :subdomain => 'bar', :memberships => [Factory.build(:membership, :account => @account)])
@account.stubs(:sites).returns([@site_1, @site_2])
Site.any_instance.stubs(:save).returns(true)
end
it 'should also delete memberships' do
Site.any_instance.stubs(:admin_memberships).returns(['junk'])
@account.destroy
@site_1.memberships.should be_empty
@site_2.memberships.should be_empty
end
it 'should raise an exception if account is the only remaining admin' do
@site_1.stubs(:admin_memberships).returns(['junk'])
lambda {
@account.destroy
}.should raise_error(Exception, "One admin account is required at least")
end
end
end end

View File

@ -8,7 +8,7 @@ describe LiquidTemplate do
# Validations ## # Validations ##
%w{site name slug value}.each do |field| %w{site name value}.each do |field|
it "should validate presence of #{field}" do it "should validate presence of #{field}" do
template = Factory.build(:liquid_template, field.to_sym => nil) template = Factory.build(:liquid_template, field.to_sym => nil)
template.should_not be_valid template.should_not be_valid

View File

@ -0,0 +1,55 @@
require 'spec_helper'
describe Membership do
it 'should have a valid factory' do
Factory.build(:membership, :account => Factory.build(:account)).should be_valid
end
it 'should validate presence of account' do
membership = Factory.build(:membership, :account => nil)
membership.should_not be_valid
membership.errors[:account].should == ["can't be blank"]
end
it 'should assign account from email' do
Account.stubs(:where).returns([Factory.build(:account)])
Account.stubs(:find).returns(Factory.build(:account))
membership = Factory.build(:membership, :account => nil)
membership.email = 'bart@simpson.net'
membership.account.should_not be_nil
membership.account.name.should == 'Bart Simpson'
end
describe 'next action to take' do
before(:each) do
@membership = Factory.build(:membership, :site => Factory.build(:site))
@account = Factory.build(:account)
Account.stubs(:where).returns([@account])
Account.stubs(:find).returns(@account)
end
it 'should tell error' do
@membership.action_to_take.should == :error
end
it 'should tell we need to create a new account' do
Account.stubs(:where).returns([])
@membership.email = 'homer@simpson'
@membership.action_to_take.should == :create_account
end
it 'should tell nothing to do' do
@membership.email = 'bart@simpson.net'
@membership.site.stubs(:memberships).returns([@membership, @membership])
@membership.action_to_take.should == :nothing
end
it 'should tell membership has to be saved' do
@membership.email = 'bart@simpson.net'
@membership.action_to_take.should == :save_it
end
end
end

View File

@ -2,6 +2,10 @@ require 'spec_helper'
describe Page do describe Page do
before(:each) do
Site.any_instance.stubs(:create_default_pages!).returns(true)
end
it 'should have a valid factory' do it 'should have a valid factory' do
Factory.build(:page).should be_valid Factory.build(:page).should be_valid
end end
@ -28,6 +32,17 @@ describe Page do
page.errors[:slug].should == ["is already taken"] page.errors[:slug].should == ["is already taken"]
end end
it 'should validate uniqueness of slug within a "folder"' do
site = Factory(:site)
root = Factory(:page, :slug => 'index', :site => site)
child_1 = Factory(:page, :slug => 'first_child', :parent => root, :site => site)
(page = Factory.build(:page, :slug => 'first_child', :parent => root, :site => site)).should_not be_valid
page.errors[:slug].should == ["is already taken"]
page.slug = 'index'
page.valid?.should be_true
end
%w{admin stylesheets images javascripts}.each do |slug| %w{admin stylesheets images javascripts}.each do |slug|
it "should consider '#{slug}' as invalid" do it "should consider '#{slug}' as invalid" do
page = Factory.build(:page, :slug => slug) page = Factory.build(:page, :slug => slug)
@ -61,6 +76,28 @@ describe Page do
end end
describe 'delete' do
before(:each) do
@page = Factory.build(:page)
end
it 'should delete index page' do
@page.stubs(:index?).returns(true)
lambda {
@page.destroy
}.should raise_error(Exception, 'You can not remove index or 404 pages')
end
it 'should delete 404 page' do
@page.stubs(:not_found?).returns(true)
lambda {
@page.destroy
}.should raise_error(Exception, 'You can not remove index or 404 pages')
end
end
describe 'accepts_nested_attributes_for used for parts' do describe 'accepts_nested_attributes_for used for parts' do
before(:each) do before(:each) do

View File

@ -47,9 +47,13 @@ describe Site do
end end
it 'should validate uniqueness of domains' do it 'should validate uniqueness of domains' do
Factory(:site, :domains => %w{www.acme.net www.acme.com}) Factory(:site, :domains => %w{www.acme.net www.acme.com})
(site = Factory.build(:site, :domains => %w{www.acme.com})).should_not be_valid (site = Factory.build(:site, :domains => %w{www.acme.com})).should_not be_valid
site.errors[:domains].should == ["www.acme.com is already taken"] site.errors[:domains].should == ["www.acme.com is already taken"]
(site = Factory.build(:site, :subdomain => 'foo', :domains => %w{acme.example.com})).should_not be_valid
site.errors[:domains].should == ["acme.example.com is already taken"]
end end
it 'should validate format of domains' do it 'should validate format of domains' do
@ -83,10 +87,11 @@ describe Site do
## Associations ## ## Associations ##
it 'should have many accounts' do it 'should have many accounts' do
account_1 = Factory(:account) site = Factory.build(:site)
account_2 = Factory(:account, :name => 'homer', :email => 'homer@simpson.net') account_1, account_2 = Factory(:account), Factory(:account, :name => 'homer', :email => 'homer@simpson.net')
site = Factory(:site, :accounts => [account_1, account_2, account_1]) site.memberships.build(:account => account_1, :admin => true)
site.account_ids.should == [account_1.id, account_2.id] site.memberships.build(:account => account_2)
site.memberships.size.should == 2
site.accounts.should == [account_1, account_2] site.accounts.should == [account_1, account_2]
end end
@ -98,4 +103,41 @@ describe Site do
site.domains_without_subdomain.should == %w{www.acme.net www.acme.com} site.domains_without_subdomain.should == %w{www.acme.net www.acme.com}
end end
describe 'once created' do
before(:each) do
@site = Factory(:site)
end
it 'should create index and 404 pages' do
@site.pages.size.should == 2
@site.pages.first.slug.should == 'index'
@site.pages.last.slug.should == '404'
end
end
describe 'delete in cascade' do
before(:each) do
@site = Factory(:site)
end
it 'should destroy pages' do
@site.pages.expects(:destroy_all)
@site.destroy
end
it 'should destroy layouts' do
@site.layouts.expects(:destroy_all)
@site.destroy
end
it 'should destroy snippets' do
@site.snippets.expects(:destroy_all)
@site.destroy
end
end
end end