diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
new file mode 100644
index 00000000..857fb4a2
--- /dev/null
+++ b/app/controllers/admin/accounts_controller.rb
@@ -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
\ No newline at end of file
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
index e12c7cf1..c71ec06e 100644
--- a/app/controllers/admin/base_controller.rb
+++ b/app/controllers/admin/base_controller.rb
@@ -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!
- flash[:success] = translate_flash_msg(:successful)
- end
-
- def flash_error!
- flash[:error] = translate_flash_msg(:failed)
- 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)
- 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]
+ def flash_success!(options = {})
+ msg = translate_flash_msg(:successful)
+ (options.has_key?(:now) && options[:now] ? flash.now : flash)[:success] = msg
+ end
+
+ def flash_error!(options = { :now => true })
+ msg = translate_flash_msg(:failed)
+ (options.has_key?(:now) && options[:now] ? flash.now : flash)[:error] = msg
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
\ No newline at end of file
diff --git a/app/controllers/admin/current_sites_controller.rb b/app/controllers/admin/current_sites_controller.rb
new file mode 100644
index 00000000..2dab6df4
--- /dev/null
+++ b/app/controllers/admin/current_sites_controller.rb
@@ -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
\ No newline at end of file
diff --git a/app/controllers/admin/layouts_controller.rb b/app/controllers/admin/layouts_controller.rb
index 6a79fb68..3de8b9f0 100644
--- a/app/controllers/admin/layouts_controller.rb
+++ b/app/controllers/admin/layouts_controller.rb
@@ -1,54 +1,56 @@
-class Admin::LayoutsController < Admin::BaseController
+module Admin
+ class LayoutsController < BaseController
- sections 'settings'
+ sections 'settings'
- def index
- @layouts = current_site.layouts
- end
+ def index
+ @layouts = current_site.layouts
+ end
- def new
- @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'
+ def new
+ @layout = current_site.layouts.build
end
- end
- def update
- @layout = current_site.layouts.find(params[:id])
+ 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
+
+ def update
+ @layout = current_site.layouts.find(params[:id])
- if @layout.update_attributes(params[:layout])
- flash_success!
- redirect_to edit_admin_layout_url(@layout)
- else
- flash_error!
- render :action => "edit"
+ if @layout.update_attributes(params[:layout])
+ flash_success!
+ redirect_to edit_admin_layout_url(@layout)
+ else
+ flash_error!
+ render :action => "edit"
+ end
end
- end
- def destroy
- @layout = current_site.layouts.find(params[:id])
+ def destroy
+ @layout = current_site.layouts.find(params[:id])
- begin
- @layout.destroy
- flash_success!
- rescue Exception => e
- flash[:error] = e.to_s
+ begin
+ @layout.destroy
+ flash_success!
+ rescue Exception => e
+ flash[:error] = e.to_s
+ end
+
+ redirect_to admin_layouts_url
end
-
- redirect_to admin_layouts_url
- end
+ end
end
\ No newline at end of file
diff --git a/app/controllers/admin/memberships_controller.rb b/app/controllers/admin/memberships_controller.rb
new file mode 100644
index 00000000..467856b3
--- /dev/null
+++ b/app/controllers/admin/memberships_controller.rb
@@ -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
\ No newline at end of file
diff --git a/app/controllers/admin/my_accounts_controller.rb b/app/controllers/admin/my_accounts_controller.rb
new file mode 100644
index 00000000..c00f07cd
--- /dev/null
+++ b/app/controllers/admin/my_accounts_controller.rb
@@ -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
\ No newline at end of file
diff --git a/app/controllers/admin/page_parts_controller.rb b/app/controllers/admin/page_parts_controller.rb
index c36948ed..0126ab45 100644
--- a/app/controllers/admin/page_parts_controller.rb
+++ b/app/controllers/admin/page_parts_controller.rb
@@ -1,10 +1,12 @@
-class Admin::PagePartsController < Admin::BaseController
+module Admin
+ class PagePartsController < BaseController
- layout nil
+ layout nil
- def index
- parts = current_site.layouts.find(params[:layout_id]).parts
- render :json => { :parts => parts }
- end
+ def index
+ parts = current_site.layouts.find(params[:layout_id]).parts
+ render :json => { :parts => parts }
+ end
-end
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/admin/pages_controller.rb b/app/controllers/admin/pages_controller.rb
index 2d778900..706e6637 100644
--- a/app/controllers/admin/pages_controller.rb
+++ b/app/controllers/admin/pages_controller.rb
@@ -1,68 +1,70 @@
-class Admin::PagesController < Admin::BaseController
+module Admin
+ class PagesController < BaseController
- sections 'contents'
+ sections 'contents'
- def index
- @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'
+ def index
+ @pages = current_site.pages.roots
end
- end
- def update
- @page = current_site.pages.find(params[:id])
+ 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
+
+ def update
+ @page = current_site.pages.find(params[:id])
- if @page.update_attributes(params[:page])
- flash_success!
- redirect_to edit_admin_page_url(@page)
- else
- flash_error!
- render :action => "edit"
+ if @page.update_attributes(params[:page])
+ flash_success!
+ redirect_to edit_admin_page_url(@page)
+ else
+ flash_error!
+ render :action => "edit"
+ end
end
- end
- def sort
- @page = current_site.pages.find(params[:id])
- @page.sort_children!(params[:children])
+ def sort
+ @page = current_site.pages.find(params[:id])
+ @page.sort_children!(params[:children])
- 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
+ render :json => { :message => translate_flash_msg(:successful) }
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
\ No newline at end of file
diff --git a/app/controllers/admin/passwords_controller.rb b/app/controllers/admin/passwords_controller.rb
index 5eed6cb0..c0bf4caa 100644
--- a/app/controllers/admin/passwords_controller.rb
+++ b/app/controllers/admin/passwords_controller.rb
@@ -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
\ No newline at end of file
diff --git a/app/controllers/admin/sessions_controller.rb b/app/controllers/admin/sessions_controller.rb
index 7f34212f..46860c13 100644
--- a/app/controllers/admin/sessions_controller.rb
+++ b/app/controllers/admin/sessions_controller.rb
@@ -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
\ No newline at end of file
diff --git a/app/controllers/admin/sites_controller.rb b/app/controllers/admin/sites_controller.rb
new file mode 100644
index 00000000..d8e61944
--- /dev/null
+++ b/app/controllers/admin/sites_controller.rb
@@ -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
\ No newline at end of file
diff --git a/app/controllers/admin/snippets_controller.rb b/app/controllers/admin/snippets_controller.rb
index a47831b6..e5eab712 100644
--- a/app/controllers/admin/snippets_controller.rb
+++ b/app/controllers/admin/snippets_controller.rb
@@ -1,54 +1,56 @@
-class Admin::SnippetsController < Admin::BaseController
+module Admin
+ class SnippetsController < BaseController
- sections 'settings'
+ sections 'settings'
- def index
- @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'
+ def index
+ @snippets = current_site.snippets
end
- end
- def update
- @snippet = current_site.snippets.find(params[:id])
+ 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
+
+ def update
+ @snippet = current_site.snippets.find(params[:id])
- if @snippet.update_attributes(params[:snippet])
- flash_success!
- redirect_to edit_admin_snippet_url(@snippet)
- else
- flash_error!
- render :action => "edit"
+ if @snippet.update_attributes(params[:snippet])
+ flash_success!
+ redirect_to edit_admin_snippet_url(@snippet)
+ else
+ flash_error!
+ render :action => "edit"
+ end
end
- end
- def destroy
- @snippet = current_site.snippets.find(params[:id])
+ def destroy
+ @snippet = current_site.snippets.find(params[:id])
- begin
- @snippet.destroy
- flash_success!
- rescue Exception => e
- flash[:error] = e.to_s
+ begin
+ @snippet.destroy
+ flash_success!
+ rescue Exception => e
+ flash[:error] = e.to_s
+ end
+
+ redirect_to admin_snippets_url
end
-
- redirect_to admin_snippets_url
- end
+ end
end
\ No newline at end of file
diff --git a/app/helpers/admin/accounts_helper.rb b/app/helpers/admin/accounts_helper.rb
new file mode 100644
index 00000000..fa83b42d
--- /dev/null
+++ b/app/helpers/admin/accounts_helper.rb
@@ -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
+
\ No newline at end of file
diff --git a/app/helpers/admin/sites_helper.rb b/app/helpers/admin/sites_helper.rb
new file mode 100644
index 00000000..193efd9f
--- /dev/null
+++ b/app/helpers/admin/sites_helper.rb
@@ -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
\ No newline at end of file
diff --git a/app/models/account.rb b/app/models/account.rb
index 04b2477b..4ba59b4d 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -3,7 +3,7 @@ class Account
include Mongoid::Timestamps
# devise modules
- devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable #:registerable,
+ devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable
# attr_accessible :email, :password, :password_confirmation # TODO
@@ -16,8 +16,31 @@ class Account
## associations ##
+ ## callbacks ##
+ before_destroy :remove_memberships!
+
+ ## methods ##
+
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
diff --git a/app/models/layout.rb b/app/models/layout.rb
index e1dc2b16..bedbab44 100644
--- a/app/models/layout.rb
+++ b/app/models/layout.rb
@@ -28,7 +28,7 @@ class Layout < LiquidTemplate
slug = part[0].strip.downcase
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
self.parts.build :slug => slug, :name => (part[1] || slug).gsub("\"", '')
end
diff --git a/app/models/membership.rb b/app/models/membership.rb
new file mode 100644
index 00000000..b7116e39
--- /dev/null
+++ b/app/models/membership.rb
@@ -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
\ No newline at end of file
diff --git a/app/models/page.rb b/app/models/page.rb
index 00ebb354..41b0101c 100644
--- a/app/models/page.rb
+++ b/app/models/page.rb
@@ -21,21 +21,21 @@ class Page
before_validate :normalize_slug
before_save { |p| p.parent_id = nil if p.parent_id.blank? }
before_save :change_parent
+ before_create { |p| p.parts << PagePart.build_body_part }
before_create { |p| p.fix_position(false) }
before_create :add_to_list_bottom
- # before_create :add_body_part
- before_destroy :remove_from_list
+ before_destroy :do_not_remove_index_and_404_pages
+ before_destroy :remove_from_list
## validations ##
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 }
## named scopes ##
## behaviours ##
acts_as_tree :order => ['position', 'asc']
- # accepts_nested_attributes_for :parts, :allow_destroy => true
## methods ##
@@ -50,10 +50,6 @@ class Page
def parts_attributes=(attributes)
self.update_parts(attributes.values.map { |attrs| PagePart.new(attrs) })
end
-
- def add_body_part
- self.parts.build :name => 'body', :slug => 'layout', :value => '---body here---'
- end
def parent=(owner) # missing in acts_as_tree
@_parent = owner
@@ -87,6 +83,17 @@ class Page
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)
performed = []
@@ -143,6 +150,8 @@ class Page
end
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|
p.position -= 1
p.save
diff --git a/app/models/page_part.rb b/app/models/page_part.rb
index 455c7c22..4567c708 100644
--- a/app/models/page_part.rb
+++ b/app/models/page_part.rb
@@ -1,6 +1,5 @@
class PagePart
include Mongoid::Document
- # include Mongoid::Timestamps
## fields ##
field :name, :type => String
@@ -12,8 +11,6 @@ class PagePart
## associations ##
embedded_in :page, :inverse_of => :parts
- # attr_accessor :_delete
-
## callbacks ##
# before_validate { |p| p.slug ||= p.name.slugify if p.name.present? }
@@ -25,12 +22,11 @@ class PagePart
## 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')
+ self.new({
+ :name => I18n.t('attributes.defaults.page_parts.name'),
+ :value => I18n.t('attributes.defaults.pages.other.body'),
+ :slug => 'layout'
+ })
end
end
\ No newline at end of file
diff --git a/app/models/site.rb b/app/models/site.rb
index 58e1ad6b..663e4f36 100644
--- a/app/models/site.rb
+++ b/app/models/site.rb
@@ -6,12 +6,12 @@ class Site
field :name
field :subdomain, :type => String
field :domains, :type => Array, :default => []
- field :account_ids, :type => Array, :default => []
## associations ##
has_many_related :pages
has_many_related :layouts
has_many_related :snippets
+ embeds_many :memberships
## validations ##
validates_presence_of :name, :subdomain
@@ -21,23 +21,24 @@ class Site
validate :domains_must_be_valid_and_unique
## callbacks ##
+ after_create :create_default_pages!
before_save :add_subdomain_to_domains
+ after_destroy :destroy_in_cascade!
## named scopes ##
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 ##
- add_dirty_methods :domains
## methods ##
def accounts
- Account.criteria.in(:_id => self.account_ids)
+ Account.criteria.in(:_id => self.memberships.collect(&:account_id))
end
- def accounts=(models_or_ids)
- self.account_ids = [*models_or_ids].collect { |object| object.respond_to?(:to_i) ? object : object.id }.uniq
+ def admin_memberships
+ self.memberships.find_all { |m| m.admin? }
end
def add_subdomain_to_domains
@@ -49,13 +50,17 @@ class Site
(self.domains || []) - ["#{self.subdomain}.#{Locomotive.config.default_domain}"]
end
+ def domains_with_subdomain
+ ((self.domains || []) + ["#{self.subdomain}.#{Locomotive.config.default_domain}"]).uniq
+ end
+
protected
def domains_must_be_valid_and_unique
- return if self.domains.empty? || (!self.new_record? && !self.domains_changed?)
-
- (self.domains_without_subdomain - (self.domains_was || [])) .each do |domain|
- if not self.class.match_domain_with_exclusion_of(domain, self).first.nil?
+ return if self.domains.empty?
+
+ self.domains_without_subdomain.each do |domain|
+ if not self.class.match_domain_with_exclusion_of(domain, self).empty?
self.errors.add(:domains, :domain_taken, :value => domain)
end
@@ -65,4 +70,20 @@ class Site
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
\ No newline at end of file
diff --git a/app/views/admin/accounts/new.html.haml b/app/views/admin/accounts/new.html.haml
new file mode 100644
index 00000000..af0ddd61
--- /dev/null
+++ b/app/views/admin/accounts/new.html.haml
@@ -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
\ No newline at end of file
diff --git a/app/views/admin/current_sites/_form.html.haml b/app/views/admin/current_sites/_form.html.haml
new file mode 100644
index 00000000..cc1612f0
--- /dev/null
+++ b/app/views/admin/current_sites/_form.html.haml
@@ -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
+
+ = 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'
+
+ %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
\ No newline at end of file
diff --git a/app/views/admin/current_sites/edit.html.haml b/app/views/admin/current_sites/edit.html.haml
new file mode 100644
index 00000000..d8e8ea28
--- /dev/null
+++ b/app/views/admin/current_sites/edit.html.haml
@@ -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
diff --git a/app/views/admin/memberships/new.html.haml b/app/views/admin/memberships/new.html.haml
new file mode 100644
index 00000000..b15c304c
--- /dev/null
+++ b/app/views/admin/memberships/new.html.haml
@@ -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
\ No newline at end of file
diff --git a/app/views/admin/my_accounts/edit.html.haml b/app/views/admin/my_accounts/edit.html.haml
new file mode 100644
index 00000000..4a1a97cf
--- /dev/null
+++ b/app/views/admin/my_accounts/edit.html.haml
@@ -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
+
+ = t(".#{locale}")
+
+ = render 'admin/shared/form_actions', :button_label => :update
diff --git a/app/views/admin/passwords/edit.html.haml b/app/views/admin/passwords/edit.html.haml
index 89703706..79c9ecae 100644
--- a/app/views/admin/passwords/edit.html.haml
+++ b/app/views/admin/passwords/edit.html.haml
@@ -15,4 +15,4 @@
= link_to t('.link'), new_account_session_path(resource_name)
.footer
- = submit_button_tag t('buttons.change_password')
+ = submit_button_tag t('admin.buttons.change_password')
diff --git a/app/views/admin/passwords/new.html.haml b/app/views/admin/passwords/new.html.haml
index ff47ad39..36f172f2 100644
--- a/app/views/admin/passwords/new.html.haml
+++ b/app/views/admin/passwords/new.html.haml
@@ -14,4 +14,4 @@
= link_to t('.link'), new_account_session_path(resource_name)
.footer
- = submit_button_tag t('buttons.send_password')
\ No newline at end of file
+ = submit_button_tag t('admin.buttons.send_password')
\ No newline at end of file
diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml
index 27714786..0c6f53fa 100644
--- a/app/views/admin/sessions/new.html.haml
+++ b/app/views/admin/sessions/new.html.haml
@@ -12,4 +12,4 @@
= link_to t('.link'), new_password_path(resource_name)
.footer
- = submit_button_tag t('buttons.login')
+ = submit_button_tag t('admin.buttons.login')
diff --git a/app/views/admin/shared/_form_actions.html.haml b/app/views/admin/shared/_form_actions.html.haml
index f86f09b3..ad872595 100644
--- a/app/views/admin/shared/_form_actions.html.haml
+++ b/app/views/admin/shared/_form_actions.html.haml
@@ -1,7 +1,10 @@
.actions
.span-12
%p
- = link_to escape_once('← ') + t('.back'), back_url
+ - if defined?(back_url)
+ = link_to escape_once('← ') + t('.back'), back_url
+ - else
+
.span-12.last
%p
diff --git a/app/views/admin/shared/_header.html.haml b/app/views/admin/shared/_header.html.haml
index 3d55509c..116cc142 100644
--- a/app/views/admin/shared/_header.html.haml
+++ b/app/views/admin/shared/_header.html.haml
@@ -1,8 +1,8 @@
%h1= link_to current_site.name, '#'
#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= '|'
- = link_to t('.see'), '#'
+ = link_to t('.see'), main_site_url
%span= '|'
= link_to t('.logout'), destroy_account_session_url, :confirm => t('admin.messages.confirm')
\ No newline at end of file
diff --git a/app/views/admin/shared/_menu.html.haml b/app/views/admin/shared/_menu.html.haml
index c8150994..07856b2b 100644
--- a/app/views/admin/shared/_menu.html.haml
+++ b/app/views/admin/shared/_menu.html.haml
@@ -1,5 +1,5 @@
%ul#menu
= admin_menu_item('contents', admin_pages_url)
= admin_menu_item('assets', '#')
- = admin_menu_item('settings', admin_layouts_url)
+ = admin_menu_item('settings', edit_admin_current_site_url)
%li.clear
diff --git a/app/views/admin/shared/menu/_settings.html.haml b/app/views/admin/shared/menu/_settings.html.haml
index 013308c9..f8f5f34c 100644
--- a/app/views/admin/shared/menu/_settings.html.haml
+++ b/app/views/admin/shared/menu/_settings.html.haml
@@ -1,3 +1,5 @@
%ul
+ = admin_submenu_item 'site', edit_admin_current_site_url
= admin_submenu_item 'layouts', admin_layouts_url
- = admin_submenu_item 'snippets', admin_snippets_url
\ No newline at end of file
+ = admin_submenu_item 'snippets', admin_snippets_url
+ = admin_submenu_item 'account', edit_admin_my_account_url
\ No newline at end of file
diff --git a/app/views/admin/sites/_form.html.haml b/app/views/admin/sites/_form.html.haml
new file mode 100644
index 00000000..b3c9ea39
--- /dev/null
+++ b/app/views/admin/sites/_form.html.haml
@@ -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
+
+ = 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'
+
+ %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')
diff --git a/app/views/admin/sites/new.html.haml b/app/views/admin/sites/new.html.haml
new file mode 100644
index 00000000..6590e7e8
--- /dev/null
+++ b/app/views/admin/sites/new.html.haml
@@ -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
\ No newline at end of file
diff --git a/config/application.rb b/config/application.rb
index 264cba3c..448e65bf 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -43,6 +43,6 @@ module Locomotive
# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters << :password
- config.secret_token = '968a457262807c64e3ed5609882e17a774b917f5bcf2d308bd37eac4ba4d416d5692e6b13d77523fddb94c1dd603f160db8492b86b5e0203240bf339fe2aeae4'
+ config.secret_token = '968a457262807c64e3ed5609882e17a774b917f5bcf2d308bd37eac4ba4d416d5692e6b13d77523fddb94c1dd603f160db8492b86b5e0203240bf339fe2aeae4'
end
end
diff --git a/config/initializers/locomotive.rb b/config/initializers/locomotive.rb
index 5e4503f4..0f49ed88 100644
--- a/config/initializers/locomotive.rb
+++ b/config/initializers/locomotive.rb
@@ -5,5 +5,10 @@ Locomotive.configure do |config|
config.default_domain = 'example.com'
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' : '')
+
+Rails.application.config.session_store :cookie_store, {
+ :key => '_locomotive_session',
+ :domain => ".#{Locomotive.config.default_domain}"
+}
\ No newline at end of file
diff --git a/config/initializers/mongoid.rb b/config/initializers/mongoid.rb
index 25b16194..f8ce69eb 100644
--- a/config/initializers/mongoid.rb
+++ b/config/initializers/mongoid.rb
@@ -20,12 +20,30 @@ 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)
- criteria = { attribute => value, :_id.ne => document._id }
- criteria[scope] = document.send(scope) if scope
- return if document.class.where(criteria).empty?
- document.errors.add(attribute, :taken, :default => options[:message], :value => value)
+ def validate_each(document, attribute, value)
+ conditions = { attribute => value, :_id.ne => document._id }
+
+ if options.has_key?(:scope) && !options[:scope].nil?
+ [*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
+
+ # protected
+ # def key_changed?(document)
+ # (document.primary_key || {}).each do |key|
+ # return true if document.send("#{key}_changed?")
+ # end; false
+ # end
end
end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
deleted file mode 100644
index 7eb2b879..00000000
--- a/config/initializers/session_store.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-Rails.application.config.session_store :cookie_store, {
- :key => '_locomotive_session',
-}
diff --git a/config/locales/admin_ui_en.yml b/config/locales/admin_ui_en.yml
index 0d02ebfc..1081459f 100644
--- a/config/locales/admin_ui_en.yml
+++ b/config/locales/admin_ui_en.yml
@@ -1,5 +1,36 @@
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:
new:
title: Login
@@ -16,38 +47,17 @@ en:
title: Update my password
link: "→ Back to login page"
password: "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
+ password_confirmation: "Confirmation of your new password"
pages:
index:
title: Listing pages
no_items: "There are no pages for now. Just click here to create the first one."
new: new page
+ page:
+ updated_at: updated at
+ edit:
+ show: show
messages:
successful_create: "Page was successfully created."
successful_update: "Page was successfully updated."
@@ -64,20 +74,47 @@ en:
no_items: "There are no snippets for now. Just click here to create the first one."
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:
titles:
information: General information
meta: Meta
code: Code
+ credentials: Credentials
+ language: Language
+ sites: Sites
+ access_points: Access points
+ memberships: Accounts
+ membership_email: Account email
hints:
page:
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."
snippet:
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
-
\ No newline at end of file
diff --git a/config/locales/default_en.yml b/config/locales/default_en.yml
index de19b5aa..93acbfad 100644
--- a/config/locales/default_en.yml
+++ b/config/locales/default_en.yml
@@ -3,4 +3,20 @@ en:
messages:
domain_taken: "{{value}} is already taken"
invalid_domain: "{{value}} is invalid"
- missing_content_for_layout: "should contain 'content_for_layout' liquid tag"
\ No newline at end of file
+ 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"
diff --git a/config/routes.rb b/config/routes.rb
index 7cec9adf..acce8446 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -27,11 +27,17 @@ Locomotive::Application.routes.draw do |map|
end
resources :snippets
- # get 'login' => 'sessions#new', :as => :new_account_session
- # post 'login' => 'sessions#create', :as => :account_session
- # get 'logout' => 'sessions#destroy', :as => :destroy_account_session
- # resource :password, :only => [:new, :create, :edit, :update], :controller => 'devise/passwords'
+ resources :site
+
+ resource :current_site
+
+ resources :accounts
+
+ resource :my_account
+
+ resources :memberships
end
+ # magic url
match '/' => 'pages#show'
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 471b5ce5..5c718c9e 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,2 +1,5 @@
-Site.create! :name => 'Locomotive test website', :subdomain => 'test'
-Account.create :name => 'Admin', :email => 'admin@locomotiveapp.org', :password => 'locomotive', :password_confirmation => 'locomotive'
+account = 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!
diff --git a/doc/TODO b/doc/TODO
index 3d12f716..6694d382 100644
--- a/doc/TODO
+++ b/doc/TODO
@@ -1,6 +1,3 @@
-- scoping
-- devise messages in French
-- localize devise emails
x admin layout
x logout button
x slugify page
@@ -22,12 +19,31 @@ x parts js/css:
x page parts
x layout part should be always in first
x pages section (CRUD)
-- slug unique within a folder
-- 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
+x my account section (part of settings)
+x add new accounts
+x edit site settings
+x slug unique within a folder
+x layouts section
+x create new site
+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)
-- remove all pages, snippets, ...etc when destroying a website
\ No newline at end of file
+- refactoring page.rb => create module pagetree
+
+
\ No newline at end of file
diff --git a/lib/locomotive/configuration.rb b/lib/locomotive/configuration.rb
index 3fbbb8a5..380fa1b5 100644
--- a/lib/locomotive/configuration.rb
+++ b/lib/locomotive/configuration.rb
@@ -7,7 +7,8 @@ module Locomotive
:default_domain => 'rails.local.fr',
:reserved_subdomains => %w{www admin email blog webmail mail support help site sites},
# :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
diff --git a/lib/locomotive/routing/site_dispatcher.rb b/lib/locomotive/routing/site_dispatcher.rb
index 91222665..ad034622 100644
--- a/lib/locomotive/routing/site_dispatcher.rb
+++ b/lib/locomotive/routing/site_dispatcher.rb
@@ -19,17 +19,22 @@ module Locomotive
protected
def fetch_site
- @site = Site.match_domain(request.host).first
+ @current_site ||= Site.match_domain(request.host).first
end
def current_site
- @site ||= fetch_site
+ @current_site || fetch_site
end
def require_site
redirect_to application_root_url and return false if current_site.nil?
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
root_url(:host => Locomotive.config.default_domain, :port => request.port)
end
diff --git a/lib/misc_form_builder.rb b/lib/misc_form_builder.rb
index b0078ded..a11e456a 100644
--- a/lib/misc_form_builder.rb
+++ b/lib/misc_form_builder.rb
@@ -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?}")
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
diff --git a/public/images/admin/flags/en.png b/public/images/admin/flags/en.png
new file mode 100644
index 00000000..995bf73b
Binary files /dev/null and b/public/images/admin/flags/en.png differ
diff --git a/public/images/admin/flags/fr.png b/public/images/admin/flags/fr.png
new file mode 100644
index 00000000..a7d0060c
Binary files /dev/null and b/public/images/admin/flags/fr.png differ
diff --git a/public/javascripts/admin/site.js b/public/javascripts/admin/site.js
new file mode 100644
index 00000000..42b2831f
--- /dev/null
+++ b/public/javascripts/admin/site.js
@@ -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();
+ });
+
+});
\ No newline at end of file
diff --git a/public/stylesheets/admin/formtastic_changes.css b/public/stylesheets/admin/formtastic_changes.css
index 343e1f47..dd1bc744 100644
--- a/public/stylesheets/admin/formtastic_changes.css
+++ b/public/stylesheets/admin/formtastic_changes.css
@@ -217,7 +217,7 @@ form.formtastic fieldset ol li.item span.actions {
/* ___ 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; }
@@ -282,6 +282,15 @@ form.formtastic fieldset.editable-list ol li.added input:focus {
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 {
height: 42px;
background-image: url(/images/admin/form/big_item.png);
diff --git a/public/stylesheets/admin/page_parts.css b/public/stylesheets/admin/page_parts.css
index f3f2cd1c..a1e6fea6 100644
--- a/public/stylesheets/admin/page_parts.css
+++ b/public/stylesheets/admin/page_parts.css
@@ -55,6 +55,6 @@
#page-parts code textarea {
width: 880px;
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;
}
\ No newline at end of file
diff --git a/public/stylesheets/admin/plugins/codemirror/csscolors.css b/public/stylesheets/admin/plugins/codemirror/csscolors.css
index fd026724..fcbe19dd 100644
--- a/public/stylesheets/admin/plugins/codemirror/csscolors.css
+++ b/public/stylesheets/admin/plugins/codemirror/csscolors.css
@@ -4,7 +4,7 @@
font-family: monospace;
font-size: 10pt;
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 {
diff --git a/public/stylesheets/admin/plugins/codemirror/javascriptcolors.css b/public/stylesheets/admin/plugins/codemirror/javascriptcolors.css
index 9cb4739d..63524bfd 100644
--- a/public/stylesheets/admin/plugins/codemirror/javascriptcolors.css
+++ b/public/stylesheets/admin/plugins/codemirror/javascriptcolors.css
@@ -4,7 +4,7 @@
font-family: monospace;
font-size: 10pt;
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 {
diff --git a/public/stylesheets/admin/plugins/codemirror/xmlcolors.css b/public/stylesheets/admin/plugins/codemirror/xmlcolors.css
index 633f2527..9b74d827 100644
--- a/public/stylesheets/admin/plugins/codemirror/xmlcolors.css
+++ b/public/stylesheets/admin/plugins/codemirror/xmlcolors.css
@@ -4,7 +4,7 @@
font-family: monospace;
font-size: 10pt;
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 {
diff --git a/spec/factories.rb b/spec/factories.rb
index ea018a65..221f8765 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -14,6 +14,12 @@ Factory.define :account do |a|
a.locale 'en'
end
+## Memberships ##
+Factory.define :membership do |m|
+ m.association :account, :factory => :account
+ m.admin true
+end
+
## Pages ##
Factory.define :page do |p|
p.association :site, :factory => :site
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index e94a8ace..4f33b77d 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -17,8 +17,7 @@ describe Account do
end
it "should have a default locale" do
- account = Factory.build(:account, :locale => nil)
- account.should be_valid
+ account = Account.new
account.locale.should == 'en'
end
@@ -32,9 +31,35 @@ describe Account do
it 'should own many sites' do
account = Factory(:account)
- site_1 = Factory(:site, :accounts => account)
- site_2 = Factory(:site, :subdomain => 'foo', :accounts => account)
+ site_1 = Factory(:site, :memberships => [Membership.new(:account => account)])
+ site_2 = Factory(:site, :subdomain => 'foo', :memberships => [Membership.new(:account => account)])
account.sites.should == [site_1, site_2]
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
\ No newline at end of file
diff --git a/spec/models/liquid_template_spec.rb b/spec/models/liquid_template_spec.rb
index b7aec2d6..e059ad48 100644
--- a/spec/models/liquid_template_spec.rb
+++ b/spec/models/liquid_template_spec.rb
@@ -8,7 +8,7 @@ describe LiquidTemplate do
# Validations ##
- %w{site name slug value}.each do |field|
+ %w{site name value}.each do |field|
it "should validate presence of #{field}" do
template = Factory.build(:liquid_template, field.to_sym => nil)
template.should_not be_valid
diff --git a/spec/models/membership_spec.rb b/spec/models/membership_spec.rb
new file mode 100644
index 00000000..4fc3bc1d
--- /dev/null
+++ b/spec/models/membership_spec.rb
@@ -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
\ No newline at end of file
diff --git a/spec/models/page_spec.rb b/spec/models/page_spec.rb
index 9baf362e..8d61d6ae 100644
--- a/spec/models/page_spec.rb
+++ b/spec/models/page_spec.rb
@@ -2,6 +2,10 @@ require 'spec_helper'
describe Page do
+ before(:each) do
+ Site.any_instance.stubs(:create_default_pages!).returns(true)
+ end
+
it 'should have a valid factory' do
Factory.build(:page).should be_valid
end
@@ -28,6 +32,17 @@ describe Page do
page.errors[:slug].should == ["is already taken"]
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|
it "should consider '#{slug}' as invalid" do
page = Factory.build(:page, :slug => slug)
@@ -61,6 +76,28 @@ describe Page do
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
before(:each) do
diff --git a/spec/models/site_spec.rb b/spec/models/site_spec.rb
index 18f6f9a4..a6893d64 100644
--- a/spec/models/site_spec.rb
+++ b/spec/models/site_spec.rb
@@ -47,9 +47,13 @@ describe Site do
end
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.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
it 'should validate format of domains' do
@@ -83,10 +87,11 @@ describe Site do
## Associations ##
it 'should have many accounts' do
- account_1 = Factory(:account)
- account_2 = Factory(:account, :name => 'homer', :email => 'homer@simpson.net')
- site = Factory(:site, :accounts => [account_1, account_2, account_1])
- site.account_ids.should == [account_1.id, account_2.id]
+ site = Factory.build(:site)
+ account_1, account_2 = Factory(:account), Factory(:account, :name => 'homer', :email => 'homer@simpson.net')
+ site.memberships.build(:account => account_1, :admin => true)
+ site.memberships.build(:account => account_2)
+ site.memberships.size.should == 2
site.accounts.should == [account_1, account_2]
end
@@ -98,4 +103,41 @@ describe Site do
site.domains_without_subdomain.should == %w{www.acme.net www.acme.com}
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
\ No newline at end of file