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