From 54116d9ebbb37df0110f61e0e6590d4537aae072 Mon Sep 17 00:00:00 2001 From: dinedine Date: Wed, 28 Jul 2010 02:42:33 +0200 Subject: [PATCH] multi site selector with cross domain authentication --- .../admin/cross_domain_sessions_controller.rb | 33 ++ app/models/account.rb | 19 +- .../admin/cross_domain_sessions/new.html.haml | 14 + app/views/admin/sessions/new.html.haml | 2 +- app/views/admin/shared/_head.html.haml | 4 +- app/views/admin/shared/_header.html.haml | 7 +- app/views/layouts/admin/login.html.haml | 2 + config/locales/admin_ui_en.yml | 6 + config/locales/admin_ui_fr.yml | 6 + config/locales/flash.en.yml | 28 +- config/locales/flash.fr.yml | 28 +- config/routes.rb | 4 +- doc/TODO | 5 + lib/locomotive/routing/site_dispatcher.rb | 2 +- .../images/admin/plugins/selectmenu/arrow.png | Bin 0 -> 467 bytes .../admin/plugins/selectmenu/background.png | Bin 0 -> 109 bytes public/javascripts/admin/application.js | 7 + .../javascripts/admin/plugins/selectmenu.js | 527 ++++++++++++++++++ public/javascripts/admin/site.js | 5 +- .../jquery/images/ui-icons_ffffff_256x240.png | Bin 0 -> 2405 bytes public/stylesheets/admin/layout.css | 5 +- public/stylesheets/admin/login.css | 7 + .../stylesheets/admin/plugins/selectmenu.css | 64 +++ spec/models/account_spec.rb | 29 + 24 files changed, 770 insertions(+), 34 deletions(-) create mode 100644 app/controllers/admin/cross_domain_sessions_controller.rb create mode 100644 app/views/admin/cross_domain_sessions/new.html.haml create mode 100644 public/images/admin/plugins/selectmenu/arrow.png create mode 100644 public/images/admin/plugins/selectmenu/background.png create mode 100644 public/javascripts/admin/plugins/selectmenu.js create mode 100644 public/stylesheets/admin/jquery/images/ui-icons_ffffff_256x240.png create mode 100644 public/stylesheets/admin/plugins/selectmenu.css diff --git a/app/controllers/admin/cross_domain_sessions_controller.rb b/app/controllers/admin/cross_domain_sessions_controller.rb new file mode 100644 index 00000000..07df5697 --- /dev/null +++ b/app/controllers/admin/cross_domain_sessions_controller.rb @@ -0,0 +1,33 @@ +module Admin + class CrossDomainSessionsController < BaseController + + layout 'admin/login' + + skip_before_filter :verify_authenticity_token + + skip_before_filter :validate_site_membership + + skip_before_filter :set_locale, :only => :create + + before_filter :authenticate_admin!, :only => :new + + def new + site = current_admin.sites.detect { |s| s._id.to_s == params[:id] } + @target = site.domains_without_subdomain.first || site.domains_with_subdomain.first + + current_admin.reset_switch_site_token! + end + + def create + if account = Account.find_using_switch_site_token(params[:token]) + account.reset_switch_site_token! + sign_in(account) + redirect_to admin_pages_path + else + flash[:alert] = t('flash.admin.cross_domain_sessions.create.alert') + redirect_to new_admin_session_path + end + end + + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 81836dac..3159b44e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -1,3 +1,5 @@ +require 'digest' + class Account include Locomotive::Mongoid::Document @@ -10,6 +12,7 @@ class Account ## attributes ## field :name field :locale, :default => 'en' + field :switch_site_token ## validations ## validates_presence_of :name @@ -22,7 +25,21 @@ class Account ## methods ## def sites - Site.where({ 'memberships.account_id' => self._id }) + @sites ||= Site.where({ 'memberships.account_id' => self._id }) + end + + def reset_switch_site_token! + self.switch_site_token = ActiveSupport::SecureRandom.base64(8).gsub("/", "_").gsub(/=+$/, "") + self.save + end + + def self.find_using_switch_site_token(token, age = 1.minute) + return if token.blank? + self.where(:switch_site_token => token, :updated_at.gt => age.ago.utc).first + end + + def self.find_using_switch_site_token!(token, age = 1.minute) + self.find_using_switch_site_token(token, age) || raise(Mongoid::Errors::DocumentNotFound.new(self, token)) end protected diff --git a/app/views/admin/cross_domain_sessions/new.html.haml b/app/views/admin/cross_domain_sessions/new.html.haml new file mode 100644 index 00000000..d1438d77 --- /dev/null +++ b/app/views/admin/cross_domain_sessions/new.html.haml @@ -0,0 +1,14 @@ +- title t('.title') + += form_tag admin_cross_domain_sessions_url(:host => @target, :port => request.port), :method => 'post' do + + = hidden_field_tag 'token', current_admin.switch_site_token + + .inner + %p.notice= t('.notice') + + .footer + = login_button_tag t('admin.buttons.switch_to_site') + +:javascript + $(document).ready(function() { $('form').submit(); }); \ 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 92eb83a7..cb7f9281 100644 --- a/app/views/admin/sessions/new.html.haml +++ b/app/views/admin/sessions/new.html.haml @@ -1,7 +1,7 @@ - title t('.title') = semantic_form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| - = f.hidden_field :remember_me, :value => "true" + = f.hidden_field :remember_me, :value => 'true' .inner = login_flash_message diff --git a/app/views/admin/shared/_head.html.haml b/app/views/admin/shared/_head.html.haml index ee75cc91..37e51a4c 100644 --- a/app/views/admin/shared/_head.html.haml +++ b/app/views/admin/shared/_head.html.haml @@ -8,9 +8,9 @@ / [if IE] = stylesheet_link_tag 'admin/blueprint/ie', :media => 'screen' -= stylesheet_link_tag 'admin/layout', 'admin/jquery/ui', 'admin/plugins/toggle', 'admin/menu', 'admin/buttons', 'admin/formtastic', 'admin/formtastic_changes', 'admin/application', :media => 'screen', :cache => Rails.env.production? && !Locomotive.heroku? += stylesheet_link_tag 'admin/layout', 'admin/jquery/ui', 'admin/plugins/toggle', 'admin/plugins/selectmenu', 'admin/menu', 'admin/plugins/toggle','admin/buttons', 'admin/formtastic', 'admin/formtastic_changes', 'admin/application', :media => 'screen', :cache => Rails.env.production? && !Locomotive.heroku? -= javascript_include_tag 'admin/jquery', 'admin/jquery.ui', 'admin/rails', 'admin/utils', 'admin/plugins/subscribe', 'admin/plugins/shortcut', 'admin/plugins/toggle', 'admin/plugins/growl', 'admin/plugins/cookie', 'admin/application', 'admin/locales/datepicker_fr', :cache => Rails.env.production? && !Locomotive.heroku? += javascript_include_tag 'admin/jquery', 'admin/jquery.ui', 'admin/rails', 'admin/utils', 'admin/plugins/subscribe', 'admin/plugins/shortcut', 'admin/plugins/toggle', 'admin/plugins/growl', 'admin/plugins/cookie', 'admin/plugins/selectmenu', 'admin/application', 'admin/locales/datepicker_fr', :cache => Rails.env.production? && !Locomotive.heroku? %script{ :type => 'text/javascript' } = find_and_preserve(growl_message) diff --git a/app/views/admin/shared/_header.html.haml b/app/views/admin/shared/_header.html.haml index 53af63b6..a9eebdee 100644 --- a/app/views/admin/shared/_header.html.haml +++ b/app/views/admin/shared/_header.html.haml @@ -1,4 +1,9 @@ -%h1= link_to current_site.name, '#' +%h1 + - if current_admin.sites.size > 1 + = select_tag 'site', options_for_select(current_admin.sites.collect { |site| [site.name, new_admin_cross_domain_session_path(:id => site.id)] }, new_admin_cross_domain_session_path(:id => current_site.id)), :id => 'site-selector' + - else + = link_to current_site.name, admin_root_url, :class => 'single' + #global-actions-bar != t('.welcome', :name => link_to(current_admin.name, edit_admin_my_account_url)) diff --git a/app/views/layouts/admin/login.html.haml b/app/views/layouts/admin/login.html.haml index 7302e9da..184faa88 100644 --- a/app/views/layouts/admin/login.html.haml +++ b/app/views/layouts/admin/login.html.haml @@ -4,6 +4,8 @@ %head %title= escape_once("#{Locomotive.config.name} — #{current_site.name}") + = javascript_include_tag 'admin/jquery' + = stylesheet_link_tag 'admin/blueprint/screen', 'admin/login', :media => 'screen', :cache => Rails.env.production? && !Locomotive.config.heroku / [if IE] = stylesheet_link_tag('admin/blueprint/ie', :media => 'screen') diff --git a/config/locales/admin_ui_en.yml b/config/locales/admin_ui_en.yml index dd94220a..fe193492 100644 --- a/config/locales/admin_ui_en.yml +++ b/config/locales/admin_ui_en.yml @@ -5,6 +5,7 @@ en: send_password: Send change_password: Update new_item: "+ add" + switch_to_site: Go messages: confirm: Are you sure ? @@ -241,6 +242,11 @@ en: image_picker: link: Insert an image into the code + cross_domain_sessions: + new: + title: Cross-domain authentication + notice: You will be redirected to the website in a couple of seconds. + formtastic: titles: information: General information diff --git a/config/locales/admin_ui_fr.yml b/config/locales/admin_ui_fr.yml index f9b2ef5b..aaa6fbd7 100644 --- a/config/locales/admin_ui_fr.yml +++ b/config/locales/admin_ui_fr.yml @@ -27,6 +27,7 @@ fr: send_password: Envoyer change_password: Mettre à jour new_item: "+ ajouter" + switch_to_site: Aller messages: confirm: "Êtes-vous sûr(e) ?" @@ -262,6 +263,11 @@ fr: image_picker: link: Insérer une image dans le code + cross_domain_sessions: + new: + title: Transfert vers un autre site + notice: Vous allez être redirigé(e) vers le site dans quelques secondes. + formtastic: titles: information: Informations générales diff --git a/config/locales/flash.en.yml b/config/locales/flash.en.yml index 8f35d681..301b45b4 100644 --- a/config/locales/flash.en.yml +++ b/config/locales/flash.en.yml @@ -12,7 +12,7 @@ en: notice: "Pages were successfully sorted." destroy: notice: "Page was successfully deleted." - + contents: create: notice: "Content was successfully created." @@ -24,7 +24,7 @@ en: notice: "Contents were successfully sorted." destroy: notice: "Content was successfully deleted." - + content_types: create: notice: "Model was successfully created." @@ -34,12 +34,12 @@ en: alert: "Model was not updated." destroy: notice: "Model was successfully deleted." - + current_sites: update: notice: "My site was successfully updated." alert: "My site was not updated." - + layouts: create: notice: "Layout was successfully created." @@ -49,7 +49,7 @@ en: alert: "Layout was not updated." destroy: notice: "Layout was successfully deleted." - + snippets: create: notice: "Snippet was successfully created." @@ -59,7 +59,7 @@ en: alert: "Snippet was not updated." destroy: notice: "Snippet was successfully deleted." - + accounts: create: notice: "Account was successfully created." @@ -69,20 +69,20 @@ en: update: notice: "My account was successfully updated." alert: "My account was not updated." - + sites: create: notice: "Site was successfully created." alert: "Site was not created." destroy: notice: "Site was successfully deleted." - + memberships: create: notice: "Membership was successfully created." alert: "Membership was not created." already_created: "Account was already added the current site." - + asset_collections: create: notice: "Collection was successfully created." @@ -92,7 +92,7 @@ en: alert: "Collection was not updated." destroy: notice: "Collection was successfully deleted." - + assets: create: notice: "Asset was successfully created." @@ -100,7 +100,7 @@ en: update: notice: "Asset was successfully updated." alert: "Asset was not updated." - + theme_assets: create: notice: "File was successfully created." @@ -110,7 +110,11 @@ en: alert: "File was not updated." destroy: notice: "File was successfully deleted." - + custom_fields: update: alert: "Field not updated" + + cross_domain_sessions: + create: + alert: "You need to sign in" \ No newline at end of file diff --git a/config/locales/flash.fr.yml b/config/locales/flash.fr.yml index b5c2e8b7..3a908486 100644 --- a/config/locales/flash.fr.yml +++ b/config/locales/flash.fr.yml @@ -12,7 +12,7 @@ fr: notice: "Les pages ont été ordonnées avec succès." destroy: notice: "La page a été supprimée avec succès." - + contents: create: notice: "L'élément a été crée avec succès." @@ -24,7 +24,7 @@ fr: notice: "Les éléments ont été ordonnés avec succès." destroy: notice: "L'élément a été supprimé avec succès." - + content_types: create: notice: "Le modèle a été crée avec succès." @@ -34,12 +34,12 @@ fr: alert: "Le modèle n'a pas été mis à jour." destroy: notice: "Le modèle a été supprimé avec succès." - + current_sites: update: notice: "Mon site a été mis à jour avec succès." alert: "Mon site n'a pas été mis à jour." - + layouts: create: notice: "Le gabarit a été crée avec succès." @@ -49,7 +49,7 @@ fr: alert: "Le gabarit n'a pas été mis à jour." destroy: notice: "Le gabarit a été supprimé avec succès." - + snippets: create: notice: "Le snippet a été crée avec succès." @@ -59,7 +59,7 @@ fr: alert: "Le snippet n'a pas été mis à jour." destroy: notice: "Le snippet a été supprimé avec succès." - + accounts: create: notice: "Le compte a été crée avec succès." @@ -69,20 +69,20 @@ fr: update: notice: "Mon compte a été mis à jour avec succès." alert: "Mon compte n'a pas été mis à jour." - + sites: create: notice: "Le site a été crée avec succès." alert: "Le site n'a pas été crée." destroy: notice: "Le site a été supprimé avec succès." - + memberships: create: notice: "Le compte a été ajouté avec succès." alert: "Le compte n'a pas été ajouté." already_created: "Le compte a déjà été ajouté pour ce site." - + asset_collections: create: notice: "La collection a été créée avec succès." @@ -92,7 +92,7 @@ fr: alert: "La collection n'a pas été mise à jour." destroy: notice: "La collection a été supprimée avec succès." - + assets: create: notice: "Le média a été crée avec succès." @@ -100,7 +100,7 @@ fr: update: notice: "Le média a été mis à jour avec succès." alert: "Le média n'a pas été mis à jour." - + theme_assets: create: notice: "Le fichier a été crée avec succès." @@ -110,7 +110,11 @@ fr: alert: "Le fichier n'a pas été mis à jour." destroy: notice: "Le fichier a été supprimé avec succès." - + custom_fields: update: alert: "Champ non mis à jour." + + cross_domain_sessions: + create: + alert: "Vous devez vous authentifier" diff --git a/config/routes.rb b/config/routes.rb index 7cc7bae1..5908a224 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,7 +22,7 @@ Rails.application.routes.draw do |map| end resources :snippets - resources :site + resources :sites resource :current_site @@ -47,6 +47,8 @@ Rails.application.routes.draw do |map| resources :api_contents, :path => 'api/:slug/contents', :controller => 'api_contents', :only => [:create] resources :custom_fields, :path => 'custom/:parent/:slug/fields' + + resources :cross_domain_sessions, :only => [:new, :create] end # sitemap diff --git a/doc/TODO b/doc/TODO index 0e700b21..709549dc 100644 --- a/doc/TODO +++ b/doc/TODO @@ -1,6 +1,11 @@ BOARD: - refactor slugify method (use parameterize + create a module) +- site selector (cross domain authentication) + x code + - tests (rspec + cucumber) +- nice 404 page => if logged in, link to create the page +- nice error page BACKLOG: diff --git a/lib/locomotive/routing/site_dispatcher.rb b/lib/locomotive/routing/site_dispatcher.rb index 2ba48eb3..3836898f 100644 --- a/lib/locomotive/routing/site_dispatcher.rb +++ b/lib/locomotive/routing/site_dispatcher.rb @@ -31,7 +31,7 @@ module Locomotive def validate_site_membership return if current_site && current_site.accounts.include?(current_admin) - redirect_to application_root_url + sign_out_and_redirect(current_admin) end def application_root_url diff --git a/public/images/admin/plugins/selectmenu/arrow.png b/public/images/admin/plugins/selectmenu/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..998fce43d1d8a285afb282119e412c43853753f1 GIT binary patch literal 467 zcmV;^0WAKBP)Sm zLTGI3HmY^&d4e7y7e!?&CFyO9dYVDPRYK4v8phEP?K_vNT>sS^_`pBs_o?T6DtVr> zIF7efRUIiPwnjx!w6ZMglM#$kJkRT=X{zJ=Xrs}z_J4}+K(;x_f ziKb~I^zK1h9F`4i+io?O&1MBSH_VZu z8(^XXUDs(igE~l3q~=PAt@WTy>W5M&tMqrlvX4-s_-o+XF0(9)^7hJv%33TI@1|*< z7Ee|Yk}HHH&i^Td') + .insertAfter(this.element); + + //transfer tabindex + var tabindex = this.element.attr('tabindex'); + if(tabindex){ this.newelement.attr('tabindex', tabindex); } + + //save reference to select in data for ease in calling methods + this.newelement.data('selectelement', this.element); + + //menu icon + this.selectmenuIcon = $('') + .prependTo(this.newelement) + .addClass( (o.style == "popup")? 'ui-icon-triangle-2-n-s' : 'ui-icon-triangle-1-s' ); + + + //make associated form label trigger focus + $('label[for='+this.element.attr('id')+']') + .attr('for', this.ids[0]) + .bind('click', function(){ + self.newelement[0].focus(); + return false; + }); + + //click toggle for menu visibility + this.newelement + .bind('mousedown', function(event){ + self._toggle(event); + //make sure a click won't open/close instantly + if(o.style == "popup"){ + self._safemouseup = false; + setTimeout(function(){self._safemouseup = true;}, 300); + } + return false; + }) + .bind('click',function(){ + return false; + }) + .keydown(function(event){ + var ret = true; + switch (event.keyCode) { + case $.ui.keyCode.ENTER: + ret = true; + break; + case $.ui.keyCode.SPACE: + ret = false; + self._toggle(event); + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.LEFT: + ret = false; + self._moveSelection(-1); + break; + case $.ui.keyCode.DOWN: + case $.ui.keyCode.RIGHT: + ret = false; + self._moveSelection(1); + break; + case $.ui.keyCode.TAB: + ret = true; + break; + default: + ret = false; + self._typeAhead(event.keyCode, 'mouseup'); + break; + } + return ret; + }) + .bind('mouseover focus', function(){ + $(this).addClass(self.widgetBaseClass+'-focus ui-state-hover'); + }) + .bind('mouseout blur', function(){ + $(this).removeClass(self.widgetBaseClass+'-focus ui-state-hover'); + }); + + //document click closes menu + $(document) + .mousedown(function(event){ + self.close(event); + }); + + //change event on original selectmenu + this.element + .click(function(){ this._refreshValue(); }) + .focus(function(){ this.newelement[0].focus(); }); + + //create menu portion, append to body + var cornerClass = (o.style == "dropdown")? " ui-corner-bottom" : " ui-corner-all" + this.list = $('').appendTo('body'); + + //serialize selectmenu element options + var selectOptionData = []; + this.element + .find('option') + .each(function(){ + selectOptionData.push({ + value: $(this).attr('value'), + text: self._formatText(jQuery(this).text()), + selected: $(this).attr('selected'), + classes: $(this).attr('class'), + parentOptGroup: $(this).parent('optgroup').attr('label') + }); + }); + + //active state class is only used in popup style + var activeClass = (self.options.style == "popup") ? " ui-state-active" : ""; + + //write li's + for(var i in selectOptionData){ + var thisLi = $('
  • '+ selectOptionData[i].text +'
  • ') + .data('index',i) + .addClass(selectOptionData[i].classes) + .data('optionClasses', selectOptionData[i].classes|| '') + .mouseup(function(event){ + if(self._safemouseup){ + var changed = $(this).data('index') != self._selectedIndex(); + self.value($(this).data('index')); + self.select(event); + if(changed){ self.change(event); } + self.close(event,true); + } + return false; + }) + .click(function(){ + return false; + }) + .bind('mouseover focus', function(){ + self._selectedOptionLi().addClass(activeClass); + self._focusedOptionLi().removeClass(self.widgetBaseClass+'-item-focus ui-state-hover'); + $(this).removeClass('ui-state-active').addClass(self.widgetBaseClass + '-item-focus ui-state-hover'); + }) + .bind('mouseout blur', function(){ + if($(this).is( self._selectedOptionLi() )){ $(this).addClass(activeClass); } + $(this).removeClass(self.widgetBaseClass + '-item-focus ui-state-hover'); + }); + + //optgroup or not... + if(selectOptionData[i].parentOptGroup){ + var optGroupName = self.widgetBaseClass + '-group-' + selectOptionData[i].parentOptGroup; + if(this.list.find('li.' + optGroupName).size()){ + this.list.find('li.' + optGroupName + ':last ul').append(thisLi); + } + else{ + $('') + .appendTo(this.list) + .find('ul') + .append(thisLi); + } + } + else{ + thisLi.appendTo(this.list); + } + + //this allows for using the scrollbar in an overflowed list + this.list.bind('mousedown mouseup', function(){return false;}); + + //append icon if option is specified + if(o.icons){ + for(var j in o.icons){ + if(thisLi.is(o.icons[j].find)){ + thisLi + .data('optionClasses', selectOptionData[i].classes + ' ' + self.widgetBaseClass + '-hasIcon') + .addClass(self.widgetBaseClass + '-hasIcon'); + var iconClass = o.icons[j].icon || ""; + + thisLi + .find('a:eq(0)') + .prepend(''); + } + } + } + } + + //add corners to top and bottom menu items + this.list.find('li:last').addClass("ui-corner-bottom"); + if(o.style == 'popup'){ this.list.find('li:first').addClass("ui-corner-top"); } + + //transfer classes to selectmenu and list + if(o.transferClasses){ + var transferClasses = this.element.attr('class') || ''; + this.newelement.add(this.list).addClass(transferClasses); + } + + //original selectmenu width + var selectWidth = this.element.width(); + + //set menu button width + this.newelement.width( (o.width) ? o.width : selectWidth); + + //set menu width to either menuWidth option value, width option value, or select width + if(o.style == 'dropdown'){ this.list.width( (o.menuWidth) ? o.menuWidth : ((o.width) ? o.width : selectWidth)); } + else { this.list.width( (o.menuWidth) ? o.menuWidth : ((o.width) ? o.width - o.handleWidth : selectWidth - o.handleWidth)); } + + //set max height from option + if(o.maxHeight && o.maxHeight < this.list.height()){ this.list.height(o.maxHeight); } + + //save reference to actionable li's (not group label li's) + this._optionLis = this.list.find('li:not(.'+ self.widgetBaseClass +'-group)'); + + //transfer menu click to menu button + this.list + .keydown(function(event){ + var ret = true; + switch (event.keyCode) { + case $.ui.keyCode.UP: + case $.ui.keyCode.LEFT: + ret = false; + self._moveFocus(-1); + break; + case $.ui.keyCode.DOWN: + case $.ui.keyCode.RIGHT: + ret = false; + self._moveFocus(1); + break; + case $.ui.keyCode.HOME: + ret = false; + self._moveFocus(':first'); + break; + case $.ui.keyCode.PAGE_UP: + ret = false; + self._scrollPage('up'); + break; + case $.ui.keyCode.PAGE_DOWN: + ret = false; + self._scrollPage('down'); + break; + case $.ui.keyCode.END: + ret = false; + self._moveFocus(':last'); + break; + case $.ui.keyCode.ENTER: + case $.ui.keyCode.SPACE: + ret = false; + self.close(event,true); + $(event.target).parents('li:eq(0)').trigger('mouseup'); + break; + case $.ui.keyCode.TAB: + ret = true; + self.close(event,true); + break; + case $.ui.keyCode.ESCAPE: + ret = false; + self.close(event,true); + break; + default: + ret = false; + self._typeAhead(event.keyCode,'focus'); + break; + } + return ret; + }); + + //selectmenu style + if(o.style == 'dropdown'){ + this.newelement + .addClass(self.widgetBaseClass+"-dropdown"); + this.list + .addClass(self.widgetBaseClass+"-menu-dropdown"); + } + else { + this.newelement + .addClass(self.widgetBaseClass+"-popup"); + this.list + .addClass(self.widgetBaseClass+"-menu-popup"); + } + + //append status span to button + this.newelement.prepend(''+ selectOptionData[this._selectedIndex()].text +''); + + //hide original selectmenu element + this.element.hide(); + + //transfer disabled state + if(this.element.attr('disabled') == true){ this.disable(); } + + //update value + this.value(this._selectedIndex()); + }, + destroy: function() { + this.element.removeData(this.widgetName) + .removeClass(this.widgetBaseClass + '-disabled' + ' ' + this.namespace + '-state-disabled') + .removeAttr('aria-disabled'); + + //unbind click on label, reset its for attr + $('label[for='+this.newelement.attr('id')+']') + .attr('for',this.element.attr('id')) + .unbind('click'); + this.newelement.remove(); + this.list.remove(); + this.element.show(); + }, + _typeAhead: function(code, eventType){ + var self = this; + //define self._prevChar if needed + if(!self._prevChar){ self._prevChar = ['',0]; } + var C = String.fromCharCode(code); + c = C.toLowerCase(); + var focusFound = false; + function focusOpt(elem, ind){ + focusFound = true; + $(elem).trigger(eventType); + self._prevChar[1] = ind; + }; + this.list.find('li a').each(function(i){ + if(!focusFound){ + var thisText = $(this).text(); + if( thisText.indexOf(C) == 0 || thisText.indexOf(c) == 0){ + if(self._prevChar[0] == C){ + if(self._prevChar[1] < i){ focusOpt(this,i); } + } + else{ focusOpt(this,i); } + } + } + }); + this._prevChar[0] = C; + }, + _uiHash: function(){ + return { + index: this.value(), + value: this.element[0].options[this.value()].value + }; + }, + open: function(event){ + var self = this; + var disabledStatus = this.newelement.attr("aria-disabled"); + if(disabledStatus != 'true'){ + this._refreshPosition(); + this._closeOthers(event); + this.newelement + .addClass('ui-state-active'); + + this.list + .appendTo('body') + .addClass(self.widgetBaseClass + '-open') + .attr('aria-hidden', false) + .find('li:not(.'+ self.widgetBaseClass +'-group):eq('+ this._selectedIndex() +') a')[0].focus(); + if(this.options.style == "dropdown"){ this.newelement.removeClass('ui-corner-all').addClass('ui-corner-top'); } + this._refreshPosition(); + this._trigger("open", event, this._uiHash()); + } + }, + close: function(event, retainFocus){ + if(this.newelement.is('.ui-state-active')){ + this.newelement + .removeClass('ui-state-active'); + this.list + .attr('aria-hidden', true) + .removeClass(this.widgetBaseClass+'-open'); + if(this.options.style == "dropdown"){ this.newelement.removeClass('ui-corner-top').addClass('ui-corner-all'); } + if(retainFocus){this.newelement[0].focus();} + this._trigger("close", event, this._uiHash()); + } + }, + change: function(event) { + this.element.trigger('change'); + this._trigger("change", event, this._uiHash()); + }, + select: function(event) { + this._trigger("select", event, this._uiHash()); + }, + _closeOthers: function(event){ + $('.'+ this.widgetBaseClass +'.ui-state-active').not(this.newelement).each(function(){ + $(this).data('selectelement').selectmenu('close',event); + }); + $('.'+ this.widgetBaseClass +'.ui-state-hover').trigger('mouseout'); + }, + _toggle: function(event,retainFocus){ + if(this.list.is('.'+ this.widgetBaseClass +'-open')){ this.close(event,retainFocus); } + else { this.open(event); } + }, + _formatText: function(text){ + return this.options.format ? this.options.format(text) : text; + }, + _selectedIndex: function(){ + return this.element[0].selectedIndex; + }, + _selectedOptionLi: function(){ + return this._optionLis.eq(this._selectedIndex()); + }, + _focusedOptionLi: function(){ + return this.list.find('.'+ this.widgetBaseClass +'-item-focus'); + }, + _moveSelection: function(amt){ + var currIndex = parseInt(this._selectedOptionLi().data('index'), 10); + var newIndex = currIndex + amt; + return this._optionLis.eq(newIndex).trigger('mouseup'); + }, + _moveFocus: function(amt){ + if(!isNaN(amt)){ + var currIndex = parseInt(this._focusedOptionLi().data('index'), 10); + var newIndex = currIndex + amt; + } + else { var newIndex = parseInt(this._optionLis.filter(amt).data('index'), 10); } + + if(newIndex < 0){ newIndex = 0; } + if(newIndex > this._optionLis.size()-1){ + newIndex = this._optionLis.size()-1; + } + var activeID = this.widgetBaseClass + '-item-' + Math.round(Math.random() * 1000); + + this._focusedOptionLi().find('a:eq(0)').attr('id',''); + this._optionLis.eq(newIndex).find('a:eq(0)').attr('id',activeID)[0].focus(); + this.list.attr('aria-activedescendant', activeID); + }, + _scrollPage: function(direction){ + var numPerPage = Math.floor(this.list.outerHeight() / this.list.find('li:first').outerHeight()); + numPerPage = (direction == 'up') ? -numPerPage : numPerPage; + this._moveFocus(numPerPage); + }, + _setData: function(key, value) { + this.options[key] = value; + if (key == 'disabled') { + this.close(); + this.element + .add(this.newelement) + .add(this.list) + [value ? 'addClass' : 'removeClass']( + this.widgetBaseClass + '-disabled' + ' ' + + this.namespace + '-state-disabled') + .attr("aria-disabled", value); + } + }, + value: function(newValue) { + if (arguments.length) { + this.element[0].selectedIndex = newValue; + this._refreshValue(); + this._refreshPosition(); + } + return this.element[0].selectedIndex; + }, + _refreshValue: function() { + var activeClass = (this.options.style == "popup") ? " ui-state-active" : ""; + var activeID = this.widgetBaseClass + '-item-' + Math.round(Math.random() * 1000); + //deselect previous + this.list + .find('.'+ this.widgetBaseClass +'-item-selected') + .removeClass(this.widgetBaseClass + "-item-selected" + activeClass) + .find('a') + .attr('aria-selected', 'false') + .attr('id', ''); + //select new + this._selectedOptionLi() + .addClass(this.widgetBaseClass + "-item-selected"+activeClass) + .find('a') + .attr('aria-selected', 'true') + .attr('id', activeID); + + //toggle any class brought in from option + var currentOptionClasses = this.newelement.data('optionClasses') ? this.newelement.data('optionClasses') : ""; + var newOptionClasses = this._selectedOptionLi().data('optionClasses') ? this._selectedOptionLi().data('optionClasses') : ""; + this.newelement + .removeClass(currentOptionClasses) + .data('optionClasses', newOptionClasses) + .addClass( newOptionClasses ) + .find('.'+this.widgetBaseClass+'-status') + .html( + this._selectedOptionLi() + .find('a:eq(0)') + .html() + ); + + this.list.attr('aria-activedescendant', activeID) + }, + _refreshPosition: function(){ + //set left value + this.list.css('left', this.newelement.offset().left); + + //set top value + var menuTop = this.newelement.offset().top; + menuTop+=this.options.offsetTop; + var scrolledAmt = this.list[0].scrollTop; + this.list.find('li:lt('+this._selectedIndex()+')').each(function(){ + scrolledAmt -= $(this).outerHeight(); + }); + + if(this.newelement.is('.'+this.widgetBaseClass+'-popup')){ + menuTop+=scrolledAmt; + this.list.css('top', menuTop); + } + else { + menuTop += this.newelement.height(); + this.list.css('top', menuTop); + } + } +}); + +$.extend($.ui.selectmenu, { + getter: "value", + version: "@VERSION", + eventPrefix: "selectmenu", + defaults: { + transferClasses: true, + style: 'popup', + width: null, + menuWidth: null, + handleWidth: 26, + maxHeight: null, + icons: null, + format: null, + offsetTop: 0 + } +}); + +})(jQuery); \ No newline at end of file diff --git a/public/javascripts/admin/site.js b/public/javascripts/admin/site.js index f9b89f40..19516a70 100644 --- a/public/javascripts/admin/site.js +++ b/public/javascripts/admin/site.js @@ -34,6 +34,9 @@ $(document).ready(function() { }); $.subscribe('form.saved.success', function(event, data) { - $('#header h1 a').html($('#current_site_name').val()); + var value = $('#current_site_name').val(); + $('#header h1 a.single').html(value); + $('#header h1 a span.ui-selectmenu-status').html(value); + $('#site-selector-menu li.ui-selectmenu-item-selected a').html(value); }, []); }); diff --git a/public/stylesheets/admin/jquery/images/ui-icons_ffffff_256x240.png b/public/stylesheets/admin/jquery/images/ui-icons_ffffff_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..4463b3a8c9be238ae5cb5f69ffb558a52973c7e8 GIT binary patch literal 2405 zcmX|@cT|(f8ph{KLJ~nj58kDL?V8x{v;RBS2f*`us zA_T=%maeP>K@5mAMM3sr1SO$JN2Mebf!y5nc;}CqIq#V>Gta#5bAFTVz1>|=UPB%L zph*A4#RmXLHU(d>NLeqSdo0NYQjF`K7++>cOl;t9!N4hqc{rF%KOPtw>=PUq6d&Cd zY$scU_Wsq+Rr+t+#S%xzB8V8D?Y>g*vD(Z70PHB;#mO(Ock0&UjY6aKuCe{nyRWTI zB>ZLYuEu*`Soq_lmja_kb6O;Mc)&3XD2g=1Kt~Z+UjXeE?>{t^tll(UTZtv$z1LNl zHq~(l7GJ*`koWz%2u&oeD1k{ZhV}(mh>U`-RMf=nya!4hna)xDL{803&lmf^Ue(Oj z<^yNa{Q{Q%JR>H0t5-cR$;q7f5pZnK9D4;U$+_nhq7?hL(R-jlL&}clLfhcEZ4DkCFr}PJJb>=m6}dm`OmLV{3j!rI7kF|@(+vhkkV$1} z%#QYA8jpC!rP-N)v&H{`cjd%yb>7&XPgW*oOJ=B2dV$L%o__F`S7YUfJLzB|Qo!k6WA;tn*b+bJXOKeotJ$DyXEh#uL9@njj z5ZDLz-p!?-F4n~7YLARc1;>qU+WhUM2f&q|3-v;v?kx2@U>)qBz8(3^(!u(;a6oxIf8NED4a2Bx7 zN&>BCFSYaHhpZ#DP_fJOu}#AcE>ABgUuyLq3rZcVJOKBOc2(Tkk+PTQJ(6^v=NEYw zI}oiDxr_Jh**~GcVkDR=xF1xrSZ#J^d=^+OIRkpy>9Xkqbi_ePPtPi)SN>UtV?QAR zYNo++mX`|Cw%k5*axALGJW5LHzEksqkygD|6mJ6{7vVT!O{aG1PiqA((qer+f@E%q z>b;X((6B`~=#$DYN4u0SR4ec`k-wy@FEc-$8v+gQPbB}U{G!{mtQ@07EjmQt`>HlP zlKbWXMuV@RMgG|v#G`GJR{w|Ws}%u56?O$R_1BF?axaosGQGsmy&AWzw<{Zk^L@ht zxB3`Mo>!Spp(*uZyXmm08+-SO+c^`Q_3#t48bzyU=1K|mjgR}WIw

    (r`L!Tzdgc%&N) z^0>mcg6D7TyZ+gbJuI;+5Zk8y9l(xT{cwUpW9z^esog6)- z@aa&nip7o$9nDz#-*6&vt;N7|A*I1ri~t2cyZtjUg9PgPKSr&~PNA4903%1fg@08O zeQMa)gU{Q9Y>Fgpth5+9ozHB(QJM*nz{GT5ygqS{~veAuY zdCaHJ=G@?9RpOxg(J%=t5ZpQ6F&;5pN)71cL~O6$G}(^u#u*fyd6Xtemw(FCB!u4< zICLVjT@w3o!Fy8+gI0SoiZa`{2DB|8uOfSV)&#a@-#c3V!UVLgB9VYfq#4cJMZ1m0 z=a*+e`MRiXt7f|^WeOLkDK-%KGDxq+Qxi6b945M0!IBzE%Yp~3O7WQksMLXD`t(M8 zlpp*U)?-kdz*jAaCyLF=_oP@m^&$TCyghi6na#wJx0&h!m`zFP$onyGC~oU;5*dzZ zb7pNZ%s+h#l_BAI<>t0y2&E$i*QuO<3Kclhz8PeArWsgarj;KD3h7|agZcG9)9cT1 zWfS;4=OAEo>&w$}aE`_Um8cRq?pGEa(MUP9pdJN*cVzX8<6lFQPZ}9PA_G;qwG^eg z(zAeN^;tRNie=h&dIS{c{WLDk|AG-ru&QwBCo{z$BIu=}@Zo!3QitsaY@u*a@7`4! z9Lf>=^M+ETI*RCqd3_91>v}DK0P0q5jPbX>KXXx{8EA{RpD_Wx2oI3!$#18?*AM-W z1$l-hH)1yHhAQS2W2y`V-=Osx4eUtp_K>SB!|atv)pL#V7w_aQA5?*xCU>2UFw9!G zSdJFu4>;wVYB7EiC7GHrfUucyjvK(&`(vk}Y}W(7D`wW}3cb)y69i35#l6D?Hl9jx zolU~OqP`*V%U(vp_(ZkVRWWR@sd-?L$CYQN=Li!6kQLD>6^Lqw%fkPx?vxAqRF_y! zrkWCUQWG6kjsw$=f>Qh&`D1{1OHGcyX4?~bGYdBs7TKjyC6wFz6gu(b8v6O$;@R%y zG4y`$hf<*~241!8=8lhs61^9W>HJ5|=aRcX?%X-|kLxaPX0ERpB3qY9AGI)FB6j4V zb<6*U?Pl-q)OJS-YCj~cQDJnHEgFb`!&V_Z58nj91J1sQ$Cm>d;;g?!* zR8Q_wk0=n}K3rH*fQE&s%Nn46VQESZ+6Wf3k#$PyHzY$8V)isQE_U5wzeIN%)@C~C vFCFf0zkh-l{!qXpIX78FGK8;_5bFUiyvsR1<>qnO2MEwzx4V=%GgAHwf1WX$ literal 0 HcmV?d00001 diff --git a/public/stylesheets/admin/layout.css b/public/stylesheets/admin/layout.css index a1c2ffed..51464f83 100644 --- a/public/stylesheets/admin/layout.css +++ b/public/stylesheets/admin/layout.css @@ -19,10 +19,11 @@ body { #header h1 { margin-bottom: 0px; + font-size: 100%; } -#header h1 a { - font-size: 0.5em; +#header h1 a.single { + font-size: 1.5em; color: #f0f0f0; text-shadow: 1px 1px 1px #000; text-decoration: none; diff --git a/public/stylesheets/admin/login.css b/public/stylesheets/admin/login.css index dbb36493..2766cb43 100644 --- a/public/stylesheets/admin/login.css +++ b/public/stylesheets/admin/login.css @@ -72,6 +72,13 @@ body { background: #000 url(/images/admin/background/body.png) repeat 0 0; } color: #CE2525; } +#panel p.notice { + font-size: 1.2em; + color: #222; + margin: 15px 49px 0 49px; + text-align: center; +} + #panel p.link { margin: 15px 0 0 49px; } diff --git a/public/stylesheets/admin/plugins/selectmenu.css b/public/stylesheets/admin/plugins/selectmenu.css new file mode 100644 index 00000000..9f70ba26 --- /dev/null +++ b/public/stylesheets/admin/plugins/selectmenu.css @@ -0,0 +1,64 @@ +/* Selectmenu +----------------------------------*/ +.ui-selectmenu { display: block; position:relative; height:2em; text-decoration: none; overflow:hidden;} +.ui-selectmenu-icon { position:absolute; right:6px; margin-top:-8px; top: 50%; } +.ui-selectmenu-menu { padding:0; margin:0; list-style:none; position:absolute; top: 0; visibility: hidden; overflow: auto; } +.ui-selectmenu-open { visibility: visible; } +.ui-selectmenu-menu-popup { margin-top: -1px; } +.ui-selectmenu-menu-dropdown { } +.ui-selectmenu-menu li { padding:0; margin:0; display: block; border-top: 1px dotted transparent; border-bottom: 1px dotted transparent; border-right-width: 0 !important; border-left-width: 0 !important; font-weight: normal !important; } +.ui-selectmenu-menu li a,.ui-selectmenu-status {line-height: 1.4em; display:block; padding:.3em 1em; outline:none; text-decoration:none; } +.ui-selectmenu-menu li.ui-selectmenu-hasIcon a, +.ui-selectmenu-hasIcon .ui-selectmenu-status { padding-left: 20px; position: relative; margin-left: 5px; } +.ui-selectmenu-menu li .ui-icon, .ui-selectmenu-status .ui-icon { position: absolute; top: 1em; margin-top: -8px; left: 0; } +.ui-selectmenu-status { line-height: 1.4em; } +.ui-selectmenu-open li.ui-selectmenu-item-focus a { } +.ui-selectmenu-open li.ui-selectmenu-item-selected { } +.ui-selectmenu-menu li span,.ui-selectmenu-status span { display:block; margin-bottom: .2em; } +.ui-selectmenu-menu li .ui-selectmenu-item-header { font-weight: bold; } +.ui-selectmenu-menu li .ui-selectmenu-item-content { } +.ui-selectmenu-menu li .ui-selectmenu-item-footer { opacity: .8; } +/*for optgroups*/ +.ui-selectmenu-menu .ui-selectmenu-group { font-size: 1em; } +.ui-selectmenu-menu .ui-selectmenu-group .ui-selectmenu-group-label { line-height: 1.4em; display:block; padding:.6em .5em 0; font-weight: bold; } +.ui-selectmenu-menu .ui-selectmenu-group ul { margin: 0; padding: 0; } + +/* site selector */ +#site-selector-button { + background: transparent; + border: none; + font-size: 1.5em; + color: #f0f0f0; + text-shadow: 1px 1px 1px #000; + margin-left: 8px; + font-family: "Helvetica Neue",Arial,Helvetica,sans-serif; + line-height: 1.5em; + height: 24px; + overflow: visible; +} + +#site-selector-button .ui-icon { background: transparent url("/images/admin/plugins/selectmenu/arrow.png") repeat 0 0; height: 9px; width: 15px; } +#site-selector-button.ui-state-active .ui-icon { background: transparent url("/images/admin/plugins/selectmenu/arrow.png") repeat 0 -9px; } + +#site-selector-button .ui-selectmenu-status { padding: 0px; line-height: 1em; float: left; } +#site-selector-button .ui-selectmenu-icon { float: left; margin: 11px 0 0 10px; position: static; top: 0px; left: 0px; } + +#site-selector-menu { + background: transparent url("/images/admin/plugins/selectmenu/background.png") repeat 0 0; + border: 1px solid transparent; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + padding: 5px 0; + z-index: 999; +} + +#site-selector-menu li a { color: #aaa; font-size: 0.8em; text-shadow: 1px 1px 1px #000; } + +#site-selector-menu .ui-corner-bottom { + -moz-border-radius: none; + -webkit-border-radius: none; + border-bottom: 1px solid transparent; +} + +#site-selector-menu.ui-selectmenu-open .ui-selectmenu-item-focus { background: transparent; border-color: transparent; } +#site-selector-menu .ui-state-hover a, #site-selector-menu .ui-state-hover a:hover { color: #fff; text-shadow: 1px 1px 1px #000; } diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 367d5f8c..49a87c6a 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -61,5 +61,34 @@ describe Account do end end + + describe 'cross domain authentication' do + + before(:each) do + @account = Factory.build(:account) + @account.stubs(:save).returns(true) + end + + it 'sets a token' do + @account.reset_switch_site_token!.should be_true + @account.switch_site_token.should_not be_empty + end + + context 'retrieving an account' do + + it 'does not find it with an empty token' do + Account.find_using_switch_site_token(nil).should be_nil + end + + it 'raises an exception if not found' do + lambda { + Account.find_using_switch_site_token!(nil) + }.should raise_error(Mongoid::Errors::DocumentNotFound) + end + + end + + + end end