Commit 1e0cc2a4e01f13aa5dc8f3d96a00591cee7f681b

Authored by Braulio Bhavamitra
2 parents c5776ba3 69b83fad

Merge branch 'fb_app' into 'master'

fb_app: plugin to create FB page tabs and share to FB timeline

Depends on !512

See merge request !513
Showing 78 changed files with 3813 additions and 265 deletions   Show diff stats
plugins/fb_app/Gemfile 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +gem 'slim'
  2 +
  3 +# for backwards compatibility of serialized objects
  4 +gem 'fb_graph'
  5 +
  6 +gem 'fb_graph2'
  7 +
  8 +gem 'facebook-signed-request'
  9 +
  10 +
... ...
plugins/fb_app/config.yml.dist 0 → 100644
... ... @@ -0,0 +1,47 @@
  1 +page_tab:
  2 + use_test_app: false
  3 +timeline:
  4 + use_test_app: true
  5 +
  6 +test_users:
  7 + - identifier1
  8 + - identifier1
  9 +
  10 +app:
  11 + id: xxx
  12 + secret: xxx
  13 + domain: domainconfigured.net
  14 +
  15 + open_graph:
  16 + namespace: app_name
  17 + objects:
  18 + blog_post: article
  19 + community: community
  20 + enterprise: sse_initiative
  21 + favorite_enterprise: sse_initiative
  22 + forum: discussion
  23 + event: event
  24 + friend: friend
  25 + gallery_image: picture
  26 + person: user
  27 + product: sse_product
  28 + uploaded_file: document
  29 + actions:
  30 + add: add
  31 + comment: comment
  32 + create: create
  33 + favorite: favorite
  34 + like: like
  35 + make_friendship: make_friendship
  36 + upload: upload
  37 + update: update
  38 + start: start
  39 + announce_creation: announce_creation
  40 + announce_new: announce_new
  41 + announce_update: announce_update
  42 + announce_news: announce_news
  43 +
  44 +test_app:
  45 + test_id: xxx
  46 + test_secret: xxx
  47 +
... ...
plugins/fb_app/controllers/myprofile/fb_app_plugin_myprofile_controller.rb 0 → 100644
... ... @@ -0,0 +1,59 @@
  1 +class FbAppPluginMyprofileController < OpenGraphPlugin::MyprofileController
  2 +
  3 + no_design_blocks
  4 +
  5 + before_filter :load_provider
  6 + before_filter :load_auth
  7 +
  8 + def index
  9 + if params[:tabs_added]
  10 + @page_tabs = FbAppPlugin::PageTab.create_from_tabs_added params[:tabs_added], params[:page_tab]
  11 + @page_tab = @page_tabs.first
  12 + redirect_to @page_tab.facebook_url
  13 + end
  14 + end
  15 +
  16 + def show_login
  17 + @status = params[:auth].delete :status
  18 + @logged_auth = FbAppPlugin::Auth.new params[:auth]
  19 + @logged_auth.fetch_user
  20 + if @auth.connected?
  21 + render partial: 'identity', locals: {auth: @logged_auth}
  22 + else
  23 + render nothing: true
  24 + end
  25 + end
  26 +
  27 + def save_auth
  28 + @status = params[:auth].delete :status rescue FbAppPlugin::Auth::Status::Unknown
  29 + if @status == FbAppPlugin::Auth::Status::Connected
  30 + @auth.attributes = params[:auth]
  31 + @auth.save! if @auth.changed?
  32 + else
  33 + @auth.destroy if @auth and @auth.persisted?
  34 + @auth = new_auth
  35 + end
  36 +
  37 + render partial: 'settings'
  38 + end
  39 +
  40 + protected
  41 +
  42 + def load_provider
  43 + @provider = FbAppPlugin.oauth_provider_for environment
  44 + end
  45 +
  46 + def load_auth
  47 + @auth = FbAppPlugin::Auth.where(profile_id: profile.id, provider_id: @provider.id).first
  48 + @auth ||= new_auth
  49 + end
  50 +
  51 + def new_auth
  52 + FbAppPlugin::Auth.new profile: profile, provider: @provider
  53 + end
  54 +
  55 + def context
  56 + :fb_app
  57 + end
  58 +
  59 +end
... ...
plugins/fb_app/controllers/public/fb_app_plugin_controller.rb 0 → 100644
... ... @@ -0,0 +1,24 @@
  1 +class FbAppPluginController < PublicController
  2 +
  3 + no_design_blocks
  4 +
  5 + def index
  6 + end
  7 +
  8 + def myprofile_config
  9 + if logged_in?
  10 + redirect_to controller: :fb_app_plugin_myprofile, profile: user.identifier
  11 + else
  12 + redirect_to controller: :account, action: :login, return_to: url_for(controller: :fb_app_plugin, action: :myprofile_config)
  13 + end
  14 + end
  15 +
  16 + protected
  17 +
  18 + # prevent session reset because X-CSRF not being passed by FB
  19 + # see also https://gist.github.com/toretore/911886
  20 + def handle_unverified_request
  21 + end
  22 +
  23 +end
  24 +
... ...
plugins/fb_app/controllers/public/fb_app_plugin_page_tab_controller.rb 0 → 100644
... ... @@ -0,0 +1,173 @@
  1 +class FbAppPluginPageTabController < FbAppPluginController
  2 +
  3 + no_design_blocks
  4 +
  5 + before_filter :change_theme
  6 + before_filter :disable_cache
  7 +
  8 + include CatalogHelper
  9 +
  10 + helper ManageProductsHelper
  11 + helper FbAppPlugin::DisplayHelper
  12 +
  13 + def index
  14 + return unless load_page_tabs
  15 +
  16 + if params[:tabs_added]
  17 + @page_tabs = FbAppPlugin::PageTab.create_from_tabs_added params[:tabs_added]
  18 + @page_tab = @page_tabs.first
  19 + redirect_to @page_tab.facebook_url
  20 + elsif @signed_request or @page_id
  21 + if @page_tab.present?
  22 + if product_id = params[:product_id]
  23 + @product = environment.products.find product_id
  24 + @profile = @product.profile
  25 + @inputs = @product.inputs
  26 + @allowed_user = false
  27 + load_catalog
  28 +
  29 + render action: 'product'
  30 + elsif @page_tab.config_type.in? [:profile, :own_profile]
  31 + @profile = @page_tab.value
  32 +
  33 + load_catalog
  34 + render action: 'catalog' unless performed?
  35 + else
  36 + # fake profile for catalog controller
  37 + @profile = environment.enterprise_default_template
  38 + @profile.shopping_cart_settings.enabled = true
  39 +
  40 + base_query = @page_tab.value
  41 + params[:base_query] = base_query
  42 + params[:scope] = 'all'
  43 +
  44 + load_catalog
  45 + render action: 'catalog' unless performed?
  46 + end
  47 + else
  48 + render action: 'first_load'
  49 + end
  50 + else
  51 + # render template
  52 + render action: 'index'
  53 + end
  54 + end
  55 +
  56 + def search_autocomplete
  57 + load_page_tabs
  58 + load_search_autocomplete
  59 + respond_to do |format|
  60 + format.json{ render 'catalog/search_autocomplete' }
  61 + end
  62 + end
  63 +
  64 + def admin
  65 + return redirect_to '/plugin/fb_app/myprofile_config' if params[:page_id].blank? and params[:signed_request].blank?
  66 + return unless load_page_tabs
  67 +
  68 + if request.put? and @page_id.present?
  69 + create_page_tabs if @page_tab.nil?
  70 +
  71 + @page_tab.update_attributes! params[:page_tab]
  72 +
  73 + respond_to do |format|
  74 + format.js{ render action: 'admin' }
  75 + end
  76 + end
  77 + end
  78 +
  79 + def destroy
  80 + @page_tab = FbAppPlugin::PageTab.find params[:id]
  81 + return render_access_denied unless user.present? and (user.is_admin?(environment) or user.is_admin? @page_tab.profile)
  82 + @page_tab.destroy
  83 + render nothing: true
  84 + end
  85 +
  86 + def uninstall
  87 + render text: params.to_yaml
  88 + end
  89 +
  90 + def enterprise_search
  91 + scope = environment.enterprises.enabled.public
  92 + @query = params[:query]
  93 + @profiles = scope.limit(10).order('name ASC').
  94 + where(['name ILIKE ? OR name ILIKE ? OR identifier LIKE ?', "#{@query}%", "% #{@query}%", "#{@query}%"])
  95 + render partial: 'open_graph_plugin/myprofile/profile_search', locals: {profiles: @profiles}
  96 + end
  97 +
  98 + # unfortunetely, this needs to be public
  99 + def profile
  100 + @profile
  101 + end
  102 +
  103 + protected
  104 +
  105 + def default_url_options
  106 + {profile: @profile.identifier} if @profile
  107 + end
  108 +
  109 + def load_page_tabs
  110 + @signed_requests = read_param params[:signed_request]
  111 + if @signed_requests.present?
  112 + @datas = []
  113 + @page_ids = @signed_requests.map do |signed_request|
  114 + @data = FbAppPlugin::Auth.parse_signed_request signed_request
  115 + @datas << @data
  116 + page_id = @data[:page][:id] rescue nil
  117 + if page_id.blank?
  118 + render_not_found
  119 + return false
  120 + end
  121 + page_id
  122 + end
  123 + else
  124 + @page_ids = read_param params[:page_id]
  125 + end
  126 +
  127 + @page_tabs = FbAppPlugin::PageTab.where page_id: @page_ids
  128 +
  129 + @signed_request = @signed_requests.first
  130 + @page_id = @page_ids.first
  131 + @page_tab = @page_tabs.first
  132 + @new_request = @page_tab.blank?
  133 +
  134 + true
  135 + end
  136 +
  137 + def create_page_tabs
  138 + @page_tabs = FbAppPlugin::PageTab.create_from_page_ids @page_ids
  139 + @page_tab ||= @page_tabs.first
  140 + end
  141 +
  142 + def change_theme
  143 + # move to config
  144 + unless theme_responsive?
  145 + @current_theme = 'ees'
  146 + @theme_responsive = true
  147 + end
  148 + @without_pure_chat = true
  149 + end
  150 + def get_layout
  151 + return nil if request.format == :js or request.xhr?
  152 +
  153 + return 'application-responsive'
  154 + end
  155 +
  156 + def disable_cache
  157 + @disable_cache_theme_navigation = true
  158 + end
  159 +
  160 + def load_catalog options = {}
  161 + @use_show_more = true
  162 + catalog_load_index options
  163 + end
  164 +
  165 + def read_param param
  166 + if param.is_a? Hash
  167 + param.values
  168 + else
  169 + Array(param).select{ |p| p.present? }
  170 + end
  171 + end
  172 +
  173 +end
... ...
plugins/fb_app/db/migrate/20140319135819_create_fb_app_page_tab_config.rb 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +class CreateFbAppPageTabConfig < ActiveRecord::Migration
  2 +
  3 + def change
  4 + create_table :fb_app_plugin_page_tab_configs do |t|
  5 + t.string :page_id
  6 + t.text :config, default: {}.to_yaml
  7 + t.integer :profile_id
  8 +
  9 + t.timestamps
  10 + end
  11 + add_index :fb_app_plugin_page_tab_configs, [:profile_id]
  12 + add_index :fb_app_plugin_page_tab_configs, [:page_id]
  13 + add_index :fb_app_plugin_page_tab_configs, [:page_id, :profile_id]
  14 + end
  15 +
  16 +end
... ...
plugins/fb_app/install.rb 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +system "script/noosfero-plugins -q enable oauth_client"
  2 +system "script/noosfero-plugins -q enable open_graph"
  3 +
... ...
plugins/fb_app/lib/ext/profile.rb 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +require_dependency 'profile'
  2 +# hate to wrte this, but without Noosfero::Plugin::Settings is loaded instead
  3 +require 'fb_app_plugin/settings'
  4 +
  5 +# attr_accessible must be defined on subclasses
  6 +Profile.descendants.each do |subclass|
  7 + subclass.class_eval do
  8 + attr_accessible :fb_app_settings
  9 + end
  10 +end
  11 +
  12 +class Profile
  13 +
  14 + def fb_app_settings attrs = {}
  15 + @fb_app_settings ||= FbAppPlugin::Settings.new self, attrs
  16 + attrs.each{ |a, v| @fb_app_settings.send "#{a}=", v }
  17 + @fb_app_settings
  18 + end
  19 + alias_method :fb_app_settings=, :fb_app_settings
  20 +
  21 + has_many :fb_app_page_tabs, class_name: 'FbAppPlugin::PageTab'
  22 +
  23 + def fb_app_auth
  24 + provider = FbAppPlugin.oauth_provider_for self.environment
  25 + self.oauth_auths.where(provider_id: provider.id).first
  26 + end
  27 +
  28 +end
... ...
plugins/fb_app/lib/fb_app_plugin.rb 0 → 100644
... ... @@ -0,0 +1,87 @@
  1 +module FbAppPlugin
  2 +
  3 + extend Noosfero::Plugin::ParentMethods
  4 +
  5 + def self.plugin_name
  6 + I18n.t 'fb_app_plugin.lib.plugin.name'
  7 + end
  8 +
  9 + def self.plugin_description
  10 + I18n.t 'fb_app_plugin.lib.plugin.description'
  11 + end
  12 +
  13 + def self.config
  14 + @config ||= HashWithIndifferentAccess.new(YAML.load File.read("#{File.dirname __FILE__}/../config.yml")) rescue {}
  15 + end
  16 +
  17 + def self.test_users
  18 + @test_users ||= self.config[:test_users]
  19 + end
  20 + def self.test_user? user
  21 + user and (self.test_users.blank? or self.test_users.include? user.identifier)
  22 + end
  23 +
  24 + def self.debug? actor=nil
  25 + self.test_user? actor
  26 + end
  27 +
  28 + def self.scope user
  29 + if self.test_user? user then 'publish_actions' else '' end
  30 + end
  31 +
  32 + def self.oauth_provider_for environment
  33 + return unless self.config.present?
  34 +
  35 + @oauth_providers ||= {}
  36 + @oauth_providers[environment] ||= begin
  37 + app_id = self.timeline_app_credentials[:id].to_s
  38 + app_secret = self.timeline_app_credentials[:secret].to_s
  39 +
  40 + client = environment.oauth_providers.where(client_id: app_id).first
  41 + # attributes that may be changed by the user
  42 + client ||= OauthClientPlugin::Provider.new strategy: 'facebook',
  43 + name: 'FB App', site: 'https://facebook.com'
  44 +
  45 + # attributes that should not change
  46 + client.attributes = {
  47 + client_id: app_id, client_secret: app_secret,
  48 + environment_id: environment.id,
  49 + }
  50 + client.save! if client.changed?
  51 +
  52 + client
  53 + end
  54 + end
  55 +
  56 + def self.open_graph_config
  57 + return unless self.config.present?
  58 +
  59 + @open_graph_config ||= begin
  60 + key = if self.config[:timeline][:use_test_app] then :test_app else :app end
  61 + self.config[key][:open_graph]
  62 + end
  63 + end
  64 +
  65 + def self.credentials app = :app
  66 + return unless self.config.present?
  67 + {id: self.config[app][:id], secret: self.config[app][:secret]}
  68 + end
  69 +
  70 + def self.timeline_app_credentials
  71 + return unless self.config.present?
  72 + @timeline_app_credentials ||= begin
  73 + key = if self.config[:timeline][:use_test_app] then :test_app else :app end
  74 + self.credentials key
  75 + end
  76 + end
  77 +
  78 + def self.page_tab_app_credentials
  79 + return unless self.config.present?
  80 + @page_tab_app_credentials ||= begin
  81 + key = if self.config[:page_tab][:use_test_app] then :test_app else :app end
  82 + self.credentials key
  83 + end
  84 + end
  85 +
  86 +end
  87 +
... ...
plugins/fb_app/lib/fb_app_plugin/base.rb 0 → 100644
... ... @@ -0,0 +1,35 @@
  1 +class FbAppPlugin::Base < Noosfero::Plugin
  2 +
  3 + def stylesheet?
  4 + true
  5 + end
  6 +
  7 + def js_files
  8 + ['fb_app.js'].map{ |j| "javascripts/#{j}" }
  9 + end
  10 +
  11 + def head_ending
  12 + return unless FbAppPlugin.config.present?
  13 + lambda do
  14 + tag 'meta', property: 'fb:app_id', content: FbAppPlugin.config[:app][:id]
  15 + end
  16 + end
  17 +
  18 + def control_panel_buttons
  19 + return unless FbAppPlugin.config.present?
  20 + { title: FbAppPlugin.plugin_name, icon: 'fb-app', url: {host: FbAppPlugin.config[:app][:domain], profile: profile.identifier, controller: :fb_app_plugin_myprofile} }
  21 + end
  22 +
  23 +end
  24 +
  25 +ActiveSupport.on_load :open_graph_plugin do
  26 + OpenGraphPlugin::Stories.register_publisher FbAppPlugin::Publisher.default
  27 +end
  28 +ActiveSupport.on_load :metadata_plugin do
  29 + MetadataPlugin::Controllers.class_eval do
  30 + def fb_app_plugin_page_tab
  31 + :@product
  32 + end
  33 + end
  34 +end
  35 +
... ...
plugins/fb_app/lib/fb_app_plugin/display_helper.rb 0 → 100644
... ... @@ -0,0 +1,51 @@
  1 +module FbAppPlugin::DisplayHelper
  2 +
  3 + extend CatalogHelper
  4 +
  5 + def fb_url_options options
  6 + options.merge! page_id: @page_ids, signed_request: @signed_requests, id: nil
  7 + end
  8 +
  9 + def url_for options = {}
  10 + return super unless options.is_a? Hash
  11 + if options[:controller] == :catalog
  12 + options[:controller] = :fb_app_plugin_page_tab
  13 + options = fb_url_options options
  14 + end
  15 + super
  16 + end
  17 +
  18 + protected
  19 +
  20 + def product_url_options product, options = {}
  21 + options = options.merge! product.url
  22 + options = options.merge! controller: :fb_app_plugin_page_tab, product_id: product.id, action: :index
  23 + options = fb_url_options options
  24 + unless Rails.env.development?
  25 + domain = FbAppPlugin.config[:app][:domain]
  26 + options[:host] = domain if domain.present?
  27 + options[:protocol] = '//'
  28 + end
  29 + options
  30 + end
  31 + def product_path product, options = {}
  32 + url = url_for product_url_options(product, options = {})
  33 + url
  34 + end
  35 +
  36 + def link_to_product product, opts = {}
  37 + url_opts = opts.delete(:url_options) || {}
  38 + url_opts = product_url_options product, url_opts
  39 + url = params.merge url_opts
  40 + link_to content_tag('span', product.name), url,
  41 + opts.merge(target: '')
  42 + end
  43 +
  44 + def link_to name = nil, options = nil, html_options = nil, &block
  45 + html_options ||= {}
  46 + options[:protocol] = '//' if options.is_a? Hash
  47 + html_options[:target] ||= '_parent'
  48 + super
  49 + end
  50 +
  51 +end
... ...
plugins/fb_app/lib/fb_app_plugin/link_renderer.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +# add target attribute to links
  2 +class FbAppPlugin::LinkRenderer < WillPaginate::ActionView::LinkRenderer
  3 +
  4 + def prepare collection, options, template
  5 + super
  6 + end
  7 +
  8 + protected
  9 +
  10 + def default_url_params
  11 + {target: ''}
  12 + end
  13 +
  14 +end
... ...
plugins/fb_app/lib/fb_app_plugin/publisher.rb 0 → 100644
... ... @@ -0,0 +1,17 @@
  1 +# Publishing examples on console
  2 +# pub=FbAppPlugin::Publisher.default; u=Profile['brauliobo']; a=Article.find 307591
  3 +# pub.publish_story a, u, :announce_news_from_a_sse_initiative
  4 +#
  5 +# pub=FbAppPlugin::Publisher.default; u=Profile['brauliobo']; f=FavoriteEnterprisePerson.last
  6 +# pub.publish_story f, u, :favorite_a_sse_initiative
  7 +#
  8 +class FbAppPlugin::Publisher < OpenGraphPlugin::Publisher
  9 +
  10 + def publish_story object_data, actor, story
  11 + OpenGraphPlugin.context = FbAppPlugin::Activity.context
  12 + a = FbAppPlugin::Activity.new object_data: object_data, actor: actor, story: story
  13 + a.dispatch_publications
  14 + a.save
  15 + end
  16 +
  17 +end
... ...
plugins/fb_app/lib/fb_app_plugin/settings.rb 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +class FbAppPlugin::Settings < OpenGraphPlugin::Settings
  2 +
  3 +end
  4 +
... ...
plugins/fb_app/locales/en-US.yml 0 → 100644
... ... @@ -0,0 +1,83 @@
  1 +
  2 +"en-US": &en-US
  3 +
  4 + fb_app_plugin:
  5 + lib:
  6 + plugin:
  7 + name: 'Facebook integration'
  8 + description: 'Use the app for Facebook!'
  9 + models:
  10 + page_tab:
  11 + types:
  12 + own_profile: 'Catalog from this enterprise'
  13 + profile: 'Catalog from a single SSE enterprise'
  14 + other_profile: 'Catalog from other SSE enterprise'
  15 + profiles: 'Catalog from more than one SSE enterprise'
  16 + query: 'Catalog of products chosen by filter or free search'
  17 + views:
  18 + myprofile:
  19 + checking_auth: 'Checking authorization'
  20 + different_login: 'But you are logged in on facebook as:'
  21 + current_login: 'You are logged in on facebook as:'
  22 + connect: 'I want to install the app for Facebook'
  23 + logged_connect: 'Connect this Facebook account with my user %{profile}'
  24 + disconnect: 'Disconnect'
  25 + connect_to_another: 'Conect to another Facebook account'
  26 + reconnect: 'Reconnect'
  27 +
  28 + timeline:
  29 + heading: 'Publishing in your facebook'
  30 + add: 'Save timeline post settings'
  31 + explanation_title: 'Soon!'
  32 + explanation_text: 'In a short time, your actions will become new posts in your Facebook timeline!'
  33 + organization_redirect: '%{redirect_link} to post updates %{type}, conect your personal profile to Facebook.'
  34 + organization_from_enterprise: 'from this enterprise'
  35 + organization_from_community: 'from this community'
  36 + redirect_link: 'Click here'
  37 +
  38 + catalogs:
  39 + heading: 'Solidarity Economy Catalog'
  40 + new: 'Add new catalog'
  41 + catalog_title_label: 'Title'
  42 + catalog_subtitle_label: 'Subtitle'
  43 + catalog_type_chooser_label: 'Type'
  44 + profile_chooser_label: 'Enterprise'
  45 + profiles_chooser_label: 'Enterprises'
  46 + query_label: "Criteria for the catalog's products"
  47 + query_help: "Write the words separated by space, and click at the button below to save."
  48 + edit_button: "Edit catalog '%{catalog_title}'"
  49 + remove_button: "Remove catalog '%{catalog_title}'"
  50 + cancel_button: 'Cancel'
  51 + confirm_removal: "<p>Warning: To really remove a catalog, you must go to the facebook page where it is, click at '<u>add or remove tabs</u>' and then remove the tab.</p><p>After that, you can come here and remove the catalog from our settings.</p><p>Are you sure you want to remove this catalog?</p>"
  52 + confirm_removal_button: 'Yes, I want to delete this catalog'
  53 + confirm_disconnect: "<p>Warning: If you disconnect your account to this Facebook profile, all your catalogs will also be removed</p><p>And then, to really remove your catalogs (if they were created), you must go to the facebook page where it is, click at '<u>add or remove tabs</u>' and then remove the tab.</p><p>After that, you can come here and disconnect.</p><p>Are you sure you want to disconnect your account to this Facebook profile?</p>"
  54 + confirm_disconnect_button: 'Yes, I want to disconnect my account from this Facebook profile'
  55 +
  56 + catalog:
  57 + see_page: 'See page on Facebook'
  58 +
  59 + error:
  60 + empty_title: 'Please add a title to your catalog.'
  61 + empty_settings: 'Please choose SSE enterprises or search terms or filters for your catalog.'
  62 +
  63 + page_tab:
  64 + edit_catalog: 'Edit catalog'
  65 + add: 'Add catalog to one of my pages on Facebook'
  66 + save: 'Save'
  67 + added_notice: "Congratulations: You've just published a new SSE catalog in Facebook!"
  68 + profile:
  69 + placeholder: "select an enterprise"
  70 + profiles:
  71 + placeholder: "select the enterprises"
  72 + query:
  73 + placeholder: "select the category or type the search terms"
  74 + back_to_catalog: 'Back to catalog'
  75 + footer1: ""
  76 + footer2: ""
  77 +
  78 +
  79 +'en_US':
  80 + <<: *en-US
  81 +'en':
  82 + <<: *en-US
  83 +
... ...
plugins/fb_app/locales/pt-BR.yml 0 → 100644
... ... @@ -0,0 +1,82 @@
  1 +
  2 +"pt-BR": &pt-BR
  3 +
  4 + fb_app_plugin:
  5 + lib:
  6 + plugin:
  7 + name: 'App Facebook'
  8 + description: 'Divulgue suas ações no Facebook!'
  9 + models:
  10 + page_tab:
  11 + types:
  12 + own_profile: 'Vitrine deste empreendimento'
  13 + profile: 'Vitrine de um único empreendimento'
  14 + other_profile: 'Vitrine de outro empreendimento'
  15 + profiles: 'Vitrine de mais de um empreendimento'
  16 + query: 'Vitrine de produtos ou serviços escolhidos por busca livre'
  17 + views:
  18 + myprofile:
  19 + checking_auth: 'Verificando autorização de acesso'
  20 + different_login: 'Mas você está logado no facebook como:'
  21 + current_login: 'Você está logado no facebook como:'
  22 + connect: 'Quero instalar o App Facebook'
  23 + logged_connect: 'Quero conectar esta conta do Facebook com meu usuário %{profile}'
  24 + disconnect: 'Desconectar'
  25 + connect_to_another: 'Conectar a outra conta do Facebook'
  26 + reconnect: 'Reconectar'
  27 +
  28 + timeline:
  29 + heading: 'Postar ações no facebook automaticamente:'
  30 + add: 'Salvar configuração de postagens na timeline'
  31 + explanation_title: 'Aguarde!'
  32 + explanation_text: 'Em breve, este aplicativo poderá postar automaticamente no seu face as ações que você fizer! Por exemplo: quando você postar novos conteúdos no seu blog, enviar uma imagem, etc.'
  33 + organization_redirect: '%{redirect_link} para postar atualizações %{type}, conecte o seu perfil pessoal ao facebook.'
  34 + organization_from_enterprise: 'deste empreendimento'
  35 + organization_from_community: 'desta comunidade'
  36 + redirect_link: 'Clique aqui'
  37 +
  38 + catalogs:
  39 + heading: 'Vitrine'
  40 + new: 'Criar nova vitrine'
  41 + catalog_title_label: 'Título'
  42 + catalog_subtitle_label: 'Subtítulo'
  43 + catalog_type_chooser_label: 'Tipo'
  44 + profile_chooser_label: 'Empreendimento'
  45 + profiles_chooser_label: 'Empreendimentos'
  46 + query_label: "Critérios para os produtos/serviços da vitrine"
  47 + query_help: "Escreva as palavras separadas por espaço, e clique no botão abaixo para salvar."
  48 + edit_button: "Editar vitrine '%{catalog_title}'"
  49 + remove_button: "Remover vitrine '%{catalog_title}'"
  50 + cancel_button: 'Cancelar'
  51 + confirm_removal: "<p><b>Atenção:</b> Para realmente apagar uma vitrine, você deve primeiro ir para a página do Facebook onde está a sua vitrine e seguir este roteiro:</p><ul><li>Clique em '<i>Mais</i>' na barra da página, depois em '<i>Gerenciar guias</i>', e então em '<i>Adicionar ou remover guias</i>'.</li><li>. Clique no X para remover.</li></ul><p>Depois disso, você pode vir aqui e pedir para remover a vitrine.</p><p>Se você já removeu lá no Facebook, tem certeza que quer remover esta vitrine?</p>"
  52 + confirm_removal_button: 'Sim, quero remover esta vitrine'
  53 + confirm_disconnect: "<p><b>Atenção:</b> Se você desconectar, vai apagar todas as vitrines. Portanto, para realmente apagar suas vitrines, você deve primeiro ir para a página do Facebook onde está a sua vitrine e seguir este roteiro:</p><ul><li>Clique em '<i>Mais</i>' na barra da página, depois em '<i>Gerenciar guias</i>', e então em '<i>Adicionar ou remover guias</i>'.</li><li>Clique no X para remover.</li></ul><p>Depois disso, você pode vir aqui e desconectar.</p><p>Tem certeza que quer desconectar?</p>"
  54 + confirm_disconnect_button: 'Sim, quero desconectar'
  55 +
  56 + catalog:
  57 + see_page: 'Ver página no facebook'
  58 +
  59 + error:
  60 + empty_title: 'Por favor, coloque um título para a sua vitrine.'
  61 + empty_settings: 'Por favor, selecione os empreendimentos solidários ou os termos de busca para sua vitrine.'
  62 +
  63 + page_tab:
  64 + edit_catalog: 'Editar vitrine'
  65 + add: 'Criar vitrine em uma página sua no Facebook'
  66 + save: 'Salvar'
  67 + added_notice: 'Parabéns: você acaba de publicar uma nova vitrine da Economia Solidária no Facebook!'
  68 + profile:
  69 + placeholder: "selecione um empreendimento"
  70 + profiles:
  71 + placeholder: "selecione os empreendimentos"
  72 + query:
  73 + placeholder: "escolha os produtos e serviços por palavras de busca"
  74 + back_to_catalog: 'Voltar à vitrine'
  75 + footer1: ""
  76 + footer2: ""
  77 +
  78 +'pt_BR':
  79 + <<: *pt-BR
  80 +'pt':
  81 + <<: *pt-BR
  82 +
... ...
plugins/fb_app/models/fb_app_plugin/activity.rb 0 → 100644
... ... @@ -0,0 +1,54 @@
  1 +class FbAppPlugin::Activity < OpenGraphPlugin::Activity
  2 +
  3 + self.context = :fb_app
  4 + self.actions = FbAppPlugin.open_graph_config[:actions]
  5 + self.objects = FbAppPlugin.open_graph_config[:objects]
  6 +
  7 + # this avoid to many saves for frequent fail cases
  8 + attr_accessor :should_save
  9 + validates_presence_of :should_save
  10 +
  11 + def self.scrape object_data_url
  12 + params = {id: object_data_url, scrape: true, method: 'post'}
  13 + url = "http://graph.facebook.com?#{params.to_query}"
  14 + Net::HTTP.get URI.parse(url)
  15 + end
  16 + def scrape
  17 + self.class.scrape self.object_data_url
  18 + end
  19 +
  20 + def publish! actor = self.actor
  21 + print_debug "fb_app: action #{self.action}, object_type #{self.object_type}" if debug? actor
  22 +
  23 + auth = actor.fb_app_auth
  24 + return if auth.blank? or auth.expired?
  25 + print_debug "fb_app: Auth found and is valid" if debug? actor
  26 +
  27 + # always update the object to expire facebook cache
  28 + Thread.new{ self.scrape }
  29 +
  30 + return if self.defs[:on] == :update and self.recent_publish? actor, self.object_type, self.object_data_url
  31 + print_debug "fb_app: no recent publication found, making new" if debug? actor
  32 +
  33 + self.should_save = true
  34 +
  35 + namespace = FbAppPlugin.open_graph_config[:namespace]
  36 + # to_str is needed to ensure String, see https://github.com/nov/fb_graph2/issues/88
  37 + params = {self.object_type => self.object_data_url.to_str}
  38 + params['fb:explicitly_shared'] = 'true' unless self.defs[:tracker]
  39 + print_debug "fb_app: publishing with params #{params.inspect}" if debug? actor
  40 +
  41 + me = FbGraph2::User.me auth.access_token
  42 + me.og_action! "#{namespace}:#{action}", params
  43 +
  44 + self.published_at = Time.now
  45 + print_debug "fb_app: published with success" if debug? actor
  46 + end
  47 +
  48 + protected
  49 +
  50 + def debug? actor=nil
  51 + super or FbAppPlugin.debug? actor
  52 + end
  53 +
  54 +end
... ...
plugins/fb_app/models/fb_app_plugin/auth.rb 0 → 100644
... ... @@ -0,0 +1,89 @@
  1 +class FbAppPlugin::Auth < OauthClientPlugin::Auth
  2 +
  3 + module Status
  4 + Connected = 'connected'
  5 + NotAuthorized = 'not_authorized'
  6 + Unknown = 'unknown'
  7 + end
  8 +
  9 + settings_items :signed_request
  10 + settings_items :fb_user
  11 +
  12 + attr_accessible :provider_user_id, :signed_request
  13 +
  14 + before_create :update_user
  15 + before_create :exchange_token
  16 + after_create :schedule_exchange_token
  17 + after_destroy :destroy_page_tabs
  18 + before_validation :set_enabled
  19 +
  20 + validates_presence_of :provider_user_id
  21 + validates_uniqueness_of :provider_user_id, scope: :profile_id
  22 +
  23 + def self.parse_signed_request signed_request, credentials = FbAppPlugin.page_tab_app_credentials
  24 + secret = credentials[:secret] rescue ''
  25 + request = Facebook::SignedRequest.new signed_request, secret: secret
  26 + request.data
  27 + end
  28 +
  29 + def status
  30 + if self.access_token.present? and self.not_expired? then Status::Connected else Status::NotAuthorized end
  31 + end
  32 + def not_authorized?
  33 + self.status == Status::NotAuthorized
  34 + end
  35 + def connected?
  36 + self.status == Status::Connected
  37 + end
  38 +
  39 + def exchange_token
  40 + app_id = FbAppPlugin.timeline_app_credentials[:id]
  41 + app_secret = FbAppPlugin.timeline_app_credentials[:secret]
  42 + fb_auth = FbGraph2::Auth.new app_id, app_secret
  43 + fb_auth.fb_exchange_token = self.access_token
  44 +
  45 + access_token = fb_auth.access_token!
  46 + self.access_token = access_token.access_token
  47 + self.expires_in = access_token.expires_in
  48 + # refresh user and its stored access token
  49 + self.fetch_user
  50 + end
  51 +
  52 + def exchange_token!
  53 + self.exchange_token
  54 + self.save!
  55 + end
  56 +
  57 + def signed_request_data
  58 + self.class.parse_signed_request self.signed_request
  59 + end
  60 +
  61 + def fetch_user
  62 + fb_user = FbGraph2::User.me self.access_token
  63 + self.fb_user = fb_user.fetch
  64 + end
  65 + def update_user
  66 + self.fb_user = self.fetch_user
  67 + end
  68 +
  69 + protected
  70 +
  71 + def destroy_page_tabs
  72 + self.profile.fb_app_page_tabs.destroy_all
  73 + end
  74 +
  75 + def exchange_token_and_reschedule!
  76 + self.exchange_token!
  77 + self.schedule_exchange_token
  78 + end
  79 +
  80 + def schedule_exchange_token
  81 + self.delay(run_at: self.expires_at - 2.weeks).exchange_token_and_reschedule!
  82 + end
  83 +
  84 + def set_enabled
  85 + self.enabled = self.not_expired?
  86 + end
  87 +
  88 +end
  89 +
... ...
plugins/fb_app/models/fb_app_plugin/page_tab.rb 0 → 100644
... ... @@ -0,0 +1,111 @@
  1 +class FbAppPlugin::PageTab < ActiveRecord::Base
  2 +
  3 + # FIXME: rename table to match model
  4 + self.table_name = :fb_app_plugin_page_tab_configs
  5 +
  6 + attr_accessible :owner_profile, :profile_id, :page_id,
  7 + :config_type, :profile_ids, :query,
  8 + :title, :subtitle
  9 +
  10 + belongs_to :owner_profile, foreign_key: :profile_id, class_name: 'Profile'
  11 +
  12 + acts_as_having_settings field: :config
  13 +
  14 + ConfigTypes = [:profile, :profiles, :query]
  15 + EnterpriseConfigTypes = [:own_profile] + ConfigTypes
  16 +
  17 + validates_presence_of :page_id
  18 + validates_uniqueness_of :page_id
  19 + validates_inclusion_of :config_type, in: ConfigTypes + EnterpriseConfigTypes
  20 +
  21 + def self.page_ids_from_tabs_added tabs_added
  22 + tabs_added.map{ |id, value| id }
  23 + end
  24 +
  25 + def self.create_from_page_ids page_ids, attrs = {}
  26 + attrs.delete :page_id
  27 + page_ids.map do |page_id|
  28 + page_tab = FbAppPlugin::PageTab.where(page_id: page_id).first
  29 + page_tab ||= FbAppPlugin::PageTab.new page_id: page_id
  30 + page_tab.update_attributes! attrs
  31 + page_tab
  32 + end
  33 + end
  34 + def self.create_from_tabs_added tabs_added, attrs = {}
  35 + page_ids = self.page_ids_from_tabs_added tabs_added
  36 + self.create_from_page_ids page_ids, attrs
  37 + end
  38 +
  39 + def self.facebook_url page_id
  40 + "https://facebook.com/#{page_id}?sk=app_#{FbAppPlugin.page_tab_app_credentials[:id]}"
  41 + end
  42 +
  43 + def facebook_url
  44 + self.class.facebook_url self.page_id
  45 + end
  46 +
  47 + def types
  48 + if self.owner_profile.present? and self.owner_profile.enterprise? then EnterpriseConfigTypes else ConfigTypes end
  49 + end
  50 +
  51 + def config_type
  52 + self.config[:type] || (self.owner_profile ? :own_profile : :profile)
  53 + end
  54 + def config_type= value
  55 + self.config[:type] = value.to_sym
  56 + end
  57 +
  58 + def value
  59 + case self.config_type
  60 + when :profiles
  61 + self.profiles.map(&:identifier).join(' OR ')
  62 + else
  63 + self.send self.config_type
  64 + end
  65 + end
  66 + def blank?
  67 + self.value.blank? rescue true
  68 + end
  69 +
  70 + def own_profile
  71 + self.owner_profile
  72 + end
  73 + def profiles
  74 + Profile.where(id: self.config[:profile_ids])
  75 + end
  76 + def profile
  77 + self.profiles.first
  78 + end
  79 + def profile_ids
  80 + self.profiles.map(&:id)
  81 + end
  82 + def query
  83 + self.config[:query]
  84 + end
  85 +
  86 + def title
  87 + self.config[:title]
  88 + end
  89 + def title= value
  90 + self.config[:title] = value
  91 + end
  92 +
  93 + def subtitle
  94 + self.config[:subtitle]
  95 + end
  96 + def subtitle= value
  97 + self.config[:subtitle] = value
  98 + end
  99 +
  100 + def profile_ids= ids
  101 + ids = ids.to_s.split(',')
  102 + self.config[:type] = if ids.size == 1 then :profile else :profiles end
  103 + self.config[:profile_ids] = ids
  104 + end
  105 +
  106 + def query= value
  107 + self.config[:type] = :query
  108 + self.config[:query] = value
  109 + end
  110 +
  111 +end
... ...
plugins/fb_app/plugins/fb_app/lib/ext/action_tracker_model.rb 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +require_dependency 'action_tracker_model'
  2 +
  3 +class ActionTracker::Record
  4 +
  5 + after_create :fb_app_publish
  6 +
  7 + protected
  8 +
  9 + def fb_app_publish
  10 + raise 'here'
  11 + end
  12 +end
... ...
plugins/fb_app/public/images/FB-f-Logo__blue_48.png 0 → 100644

722 Bytes

plugins/fb_app/public/images/cirandasnoface.png 0 → 100644

3.31 KB

plugins/fb_app/public/images/control-panel.png 0 → 120000
... ... @@ -0,0 +1 @@
  1 +FB-f-Logo__blue_48.png
0 2 \ No newline at end of file
... ...
plugins/fb_app/public/images/loading.gif 0 → 100644

18.3 KB

plugins/fb_app/public/javascripts/bootstrap-tokenfield.js 0 → 100644
... ... @@ -0,0 +1,1032 @@
  1 +/*!
  2 + * bootstrap-tokenfield
  3 + * https://github.com/sliptree/bootstrap-tokenfield
  4 + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
  5 + */
  6 +
  7 +(function (factory) {
  8 + if (typeof define === 'function' && define.amd) {
  9 + // AMD. Register as an anonymous module.
  10 + define(['jquery'], factory);
  11 + } else if (typeof exports === 'object') {
  12 + // For CommonJS and CommonJS-like environments where a window with jQuery
  13 + // is present, execute the factory with the jQuery instance from the window object
  14 + // For environments that do not inherently posses a window with a document
  15 + // (such as Node.js), expose a Tokenfield-making factory as module.exports
  16 + // This accentuates the need for the creation of a real window or passing in a jQuery instance
  17 + // e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($);
  18 + module.exports = global.window && global.window.$ ?
  19 + factory( global.window.$ ) :
  20 + function( input ) {
  21 + if ( !input.$ && !input.fn ) {
  22 + throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" );
  23 + }
  24 + return factory( input.$ || input );
  25 + };
  26 + } else {
  27 + // Browser globals
  28 + factory(jQuery, window);
  29 + }
  30 +}(function ($, window) {
  31 +
  32 + "use strict"; // jshint ;_;
  33 +
  34 + /* TOKENFIELD PUBLIC CLASS DEFINITION
  35 + * ============================== */
  36 +
  37 + var Tokenfield = function (element, options) {
  38 + var _self = this
  39 +
  40 + this.$element = $(element)
  41 + this.textDirection = this.$element.css('direction');
  42 +
  43 + // Extend options
  44 + this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options)
  45 +
  46 + // Setup delimiters and trigger keys
  47 + this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter
  48 + this._triggerKeys = $.map(this._delimiters, function (delimiter) {
  49 + return delimiter.charCodeAt(0);
  50 + });
  51 + this._firstDelimiter = this._delimiters[0];
  52 +
  53 + // Check for whitespace, dash and special characters
  54 + var whitespace = $.inArray(' ', this._delimiters)
  55 + , dash = $.inArray('-', this._delimiters)
  56 +
  57 + if (whitespace >= 0)
  58 + this._delimiters[whitespace] = '\\s'
  59 +
  60 + if (dash >= 0) {
  61 + delete this._delimiters[dash]
  62 + this._delimiters.unshift('-')
  63 + }
  64 +
  65 + var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')']
  66 + $.each(this._delimiters, function (index, character) {
  67 + var pos = $.inArray(character, specialCharacters)
  68 + if (pos >= 0) _self._delimiters[index] = '\\' + character;
  69 + });
  70 +
  71 + // Store original input width
  72 + var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null
  73 + , elStyleWidth = element.style.width
  74 + , elCSSWidth
  75 + , elWidth = this.$element.width()
  76 +
  77 + if (elRules) {
  78 + $.each( elRules, function (i, rule) {
  79 + if (rule.style.width) {
  80 + elCSSWidth = rule.style.width;
  81 + }
  82 + });
  83 + }
  84 +
  85 + // Move original input out of the way
  86 + var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left',
  87 + originalStyles = { position: this.$element.css('position') };
  88 + originalStyles[hidingPosition] = this.$element.css(hidingPosition);
  89 +
  90 + this.$element
  91 + .data('original-styles', originalStyles)
  92 + .data('original-tabindex', this.$element.prop('tabindex'))
  93 + .css('position', 'absolute')
  94 + .css(hidingPosition, '-10000px')
  95 + .prop('tabindex', -1)
  96 +
  97 + // Create a wrapper
  98 + this.$wrapper = $('<div class="tokenfield form-control" />')
  99 + if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg')
  100 + if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm')
  101 + if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl')
  102 +
  103 + // Create a new input
  104 + var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100)
  105 + this.$input = $('<input type="'+this.options.inputType+'" class="token-input" autocomplete="off" />')
  106 + .appendTo( this.$wrapper )
  107 + .prop( 'placeholder', this.$element.prop('placeholder') )
  108 + .prop( 'id', id + '-tokenfield' )
  109 + .prop( 'tabindex', this.$element.data('original-tabindex') )
  110 +
  111 + // Re-route original input label to new input
  112 + var $label = $( 'label[for="' + this.$element.prop('id') + '"]' )
  113 + if ( $label.length ) {
  114 + $label.prop( 'for', this.$input.prop('id') )
  115 + }
  116 +
  117 + // Set up a copy helper to handle copy & paste
  118 + this.$copyHelper = $('<input type="text" />').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper )
  119 +
  120 + // Set wrapper width
  121 + if (elStyleWidth) {
  122 + this.$wrapper.css('width', elStyleWidth);
  123 + }
  124 + else if (elCSSWidth) {
  125 + this.$wrapper.css('width', elCSSWidth);
  126 + }
  127 + // If input is inside inline-form with no width set, set fixed width
  128 + else if (this.$element.parents('.form-inline').length) {
  129 + this.$wrapper.width( elWidth )
  130 + }
  131 +
  132 + // Set tokenfield disabled, if original or fieldset input is disabled
  133 + if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) {
  134 + this.disable();
  135 + }
  136 +
  137 + // Set tokenfield readonly, if original input is readonly
  138 + if (this.$element.prop('readonly')) {
  139 + this.readonly();
  140 + }
  141 +
  142 + // Set up mirror for input auto-sizing
  143 + this.$mirror = $('<span style="position:absolute; top:-999px; left:0; white-space:pre;"/>');
  144 + this.$input.css('min-width', this.options.minWidth + 'px')
  145 + $.each([
  146 + 'fontFamily',
  147 + 'fontSize',
  148 + 'fontWeight',
  149 + 'fontStyle',
  150 + 'letterSpacing',
  151 + 'textTransform',
  152 + 'wordSpacing',
  153 + 'textIndent'
  154 + ], function (i, val) {
  155 + _self.$mirror[0].style[val] = _self.$input.css(val);
  156 + });
  157 + this.$mirror.appendTo( 'body' )
  158 +
  159 + // Insert tokenfield to HTML
  160 + this.$wrapper.insertBefore( this.$element )
  161 + this.$element.prependTo( this.$wrapper )
  162 +
  163 + // Calculate inner input width
  164 + this.update()
  165 +
  166 + // Create initial tokens, if any
  167 + this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens )
  168 +
  169 + // Start listening to events
  170 + this.listen()
  171 +
  172 + // Initialize autocomplete, if necessary
  173 + if ( ! $.isEmptyObject( this.options.autocomplete ) ) {
  174 + var side = this.textDirection === 'rtl' ? 'right' : 'left'
  175 + , autocompleteOptions = $.extend({
  176 + minLength: this.options.showAutocompleteOnFocus ? 0 : null,
  177 + position: { my: side + " top", at: side + " bottom", of: this.$wrapper }
  178 + }, this.options.autocomplete )
  179 +
  180 + this.$input.autocomplete( autocompleteOptions )
  181 + }
  182 +
  183 + // Initialize typeahead, if necessary
  184 + if ( ! $.isEmptyObject( this.options.typeahead ) ) {
  185 +
  186 + var typeaheadOptions = this.options.typeahead
  187 + , defaults = {
  188 + minLength: this.options.showAutocompleteOnFocus ? 0 : null
  189 + }
  190 + , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions]
  191 +
  192 + args[0] = $.extend( {}, defaults, args[0] )
  193 +
  194 + this.$input.typeahead.apply( this.$input, args )
  195 + this.typeahead = true
  196 + }
  197 + }
  198 +
  199 + Tokenfield.prototype = {
  200 +
  201 + constructor: Tokenfield
  202 +
  203 + , createToken: function (attrs, triggerChange) {
  204 + var _self = this
  205 +
  206 + if (typeof attrs === 'string') {
  207 + attrs = { value: attrs, label: attrs }
  208 + } else {
  209 + // Copy objects to prevent contamination of data sources.
  210 + attrs = $.extend( {}, attrs )
  211 + }
  212 +
  213 + if (typeof triggerChange === 'undefined') {
  214 + triggerChange = true
  215 + }
  216 +
  217 + // Normalize label and value
  218 + attrs.value = $.trim(attrs.value.toString());
  219 + attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value
  220 +
  221 + // Bail out if has no value or label, or label is too short
  222 + if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return
  223 +
  224 + // Bail out if maximum number of tokens is reached
  225 + if (this.options.limit && this.getTokens().length >= this.options.limit) return
  226 +
  227 + // Allow changing token data before creating it
  228 + var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs })
  229 + this.$element.trigger(createEvent)
  230 +
  231 + // Bail out if there if attributes are empty or event was defaultPrevented
  232 + if (!createEvent.attrs || createEvent.isDefaultPrevented()) return
  233 +
  234 + var $token = $('<div class="token" />')
  235 + .append('<span class="token-label" />')
  236 + .append('<a href="#" class="close" tabindex="-1">&times;</a>')
  237 + .data('attrs', attrs)
  238 +
  239 + // Insert token into HTML
  240 + if (this.$input.hasClass('tt-input')) {
  241 + // If the input has typeahead enabled, insert token before it's parent
  242 + this.$input.parent().before( $token )
  243 + } else {
  244 + this.$input.before( $token )
  245 + }
  246 +
  247 + // Temporarily set input width to minimum
  248 + this.$input.css('width', this.options.minWidth + 'px')
  249 +
  250 + var $tokenLabel = $token.find('.token-label')
  251 + , $closeButton = $token.find('.close')
  252 +
  253 + // Determine maximum possible token label width
  254 + if (!this.maxTokenWidth) {
  255 + this.maxTokenWidth =
  256 + this.$wrapper.width() - $closeButton.outerWidth() -
  257 + parseInt($closeButton.css('margin-left'), 10) -
  258 + parseInt($closeButton.css('margin-right'), 10) -
  259 + parseInt($token.css('border-left-width'), 10) -
  260 + parseInt($token.css('border-right-width'), 10) -
  261 + parseInt($token.css('padding-left'), 10) -
  262 + parseInt($token.css('padding-right'), 10)
  263 + parseInt($tokenLabel.css('border-left-width'), 10) -
  264 + parseInt($tokenLabel.css('border-right-width'), 10) -
  265 + parseInt($tokenLabel.css('padding-left'), 10) -
  266 + parseInt($tokenLabel.css('padding-right'), 10)
  267 + parseInt($tokenLabel.css('margin-left'), 10) -
  268 + parseInt($tokenLabel.css('margin-right'), 10)
  269 + }
  270 +
  271 + //$tokenLabel.css('max-width', this.maxTokenWidth)
  272 + if (this.options.html)
  273 + $tokenLabel.html(attrs.label)
  274 + else
  275 + $tokenLabel.text(attrs.label)
  276 +
  277 + // Listen to events on token
  278 + $token
  279 + .on('mousedown', function (e) {
  280 + if (_self._disabled || _self._readonly) return false
  281 + _self.preventDeactivation = true
  282 + })
  283 + .on('click', function (e) {
  284 + if (_self._disabled || _self._readonly) return false
  285 + _self.preventDeactivation = false
  286 +
  287 + if (e.ctrlKey || e.metaKey) {
  288 + e.preventDefault()
  289 + return _self.toggle( $token )
  290 + }
  291 +
  292 + _self.activate( $token, e.shiftKey, e.shiftKey )
  293 + })
  294 + .on('dblclick', function (e) {
  295 + if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false
  296 + _self.edit( $token )
  297 + })
  298 +
  299 + $closeButton
  300 + .on('click', $.proxy(this.remove, this))
  301 +
  302 + // Trigger createdtoken event on the original field
  303 + // indicating that the token is now in the DOM
  304 + this.$element.trigger($.Event('tokenfield:createdtoken', {
  305 + attrs: attrs,
  306 + relatedTarget: $token.get(0)
  307 + }))
  308 +
  309 + // Trigger change event on the original field
  310 + if (triggerChange) {
  311 + this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) )
  312 + }
  313 +
  314 + // Update tokenfield dimensions
  315 + this.update()
  316 +
  317 + // Return original element
  318 + return this.$element.get(0)
  319 + }
  320 +
  321 + , setTokens: function (tokens, add, triggerChange) {
  322 + if (!tokens) return
  323 +
  324 + if (!add) this.$wrapper.find('.token').remove()
  325 +
  326 + if (typeof triggerChange === 'undefined') {
  327 + triggerChange = true
  328 + }
  329 +
  330 + if (typeof tokens === 'string') {
  331 + if (this._delimiters.length) {
  332 + // Split based on delimiters
  333 + tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) )
  334 + } else {
  335 + tokens = [tokens];
  336 + }
  337 + }
  338 +
  339 + var _self = this
  340 + $.each(tokens, function (i, attrs) {
  341 + _self.createToken(attrs, triggerChange)
  342 + })
  343 +
  344 + return this.$element.get(0)
  345 + }
  346 +
  347 + , getTokenData: function($token) {
  348 + var data = $token.map(function() {
  349 + var $token = $(this);
  350 + return $token.data('attrs')
  351 + }).get();
  352 +
  353 + if (data.length == 1) {
  354 + data = data[0];
  355 + }
  356 +
  357 + return data;
  358 + }
  359 +
  360 + , getTokens: function(active) {
  361 + var self = this
  362 + , tokens = []
  363 + , activeClass = active ? '.active' : '' // get active tokens only
  364 + this.$wrapper.find( '.token' + activeClass ).each( function() {
  365 + tokens.push( self.getTokenData( $(this) ) )
  366 + })
  367 + return tokens
  368 + }
  369 +
  370 + , getTokensList: function(delimiter, beautify, active) {
  371 + delimiter = delimiter || this._firstDelimiter
  372 + beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify
  373 +
  374 + var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '')
  375 + return $.map( this.getTokens(active), function (token) {
  376 + return token.value
  377 + }).join(separator)
  378 + }
  379 +
  380 + , getInput: function() {
  381 + return this.$input.val()
  382 + }
  383 +
  384 + , listen: function () {
  385 + var _self = this
  386 +
  387 + this.$element
  388 + .on('change', $.proxy(this.change, this))
  389 +
  390 + this.$wrapper
  391 + .on('mousedown',$.proxy(this.focusInput, this))
  392 +
  393 + this.$input
  394 + .on('focus', $.proxy(this.focus, this))
  395 + .on('blur', $.proxy(this.blur, this))
  396 + .on('paste', $.proxy(this.paste, this))
  397 + .on('keydown', $.proxy(this.keydown, this))
  398 + .on('keypress', $.proxy(this.keypress, this))
  399 + .on('keyup', $.proxy(this.keyup, this))
  400 +
  401 + this.$copyHelper
  402 + .on('focus', $.proxy(this.focus, this))
  403 + .on('blur', $.proxy(this.blur, this))
  404 + .on('keydown', $.proxy(this.keydown, this))
  405 + .on('keyup', $.proxy(this.keyup, this))
  406 +
  407 + // Secondary listeners for input width calculation
  408 + this.$input
  409 + .on('keypress', $.proxy(this.update, this))
  410 + .on('keyup', $.proxy(this.update, this))
  411 +
  412 + this.$input
  413 + .on('autocompletecreate', function() {
  414 + // Set minimum autocomplete menu width
  415 + var $_menuElement = $(this).data('ui-autocomplete').menu.element
  416 +
  417 + var minWidth = _self.$wrapper.outerWidth() -
  418 + parseInt( $_menuElement.css('border-left-width'), 10 ) -
  419 + parseInt( $_menuElement.css('border-right-width'), 10 )
  420 +
  421 + $_menuElement.css( 'min-width', minWidth + 'px' )
  422 + })
  423 + .on('autocompleteselect', function (e, ui) {
  424 + if (_self.createToken( ui.item )) {
  425 + _self.$input.val('')
  426 + if (_self.$input.data( 'edit' )) {
  427 + _self.unedit(true)
  428 + }
  429 + }
  430 + return false
  431 + })
  432 + .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) {
  433 + // Create token
  434 + if (_self.createToken( datum )) {
  435 + _self.$input.typeahead('val', '')
  436 + if (_self.$input.data( 'edit' )) {
  437 + _self.unedit(true)
  438 + }
  439 + }
  440 + })
  441 +
  442 + // Listen to window resize
  443 + $(window).on('resize', $.proxy(this.update, this ))
  444 +
  445 + }
  446 +
  447 + , keydown: function (e) {
  448 +
  449 + if (!this.focused) return
  450 +
  451 + var _self = this
  452 +
  453 + switch(e.keyCode) {
  454 + case 8: // backspace
  455 + if (!this.$input.is(document.activeElement)) break
  456 + this.lastInputValue = this.$input.val()
  457 + break
  458 +
  459 + case 37: // left arrow
  460 + leftRight( this.textDirection === 'rtl' ? 'next': 'prev' )
  461 + break
  462 +
  463 + case 38: // up arrow
  464 + upDown('prev')
  465 + break
  466 +
  467 + case 39: // right arrow
  468 + leftRight( this.textDirection === 'rtl' ? 'prev': 'next' )
  469 + break
  470 +
  471 + case 40: // down arrow
  472 + upDown('next')
  473 + break
  474 +
  475 + case 65: // a (to handle ctrl + a)
  476 + if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break
  477 + this.activateAll()
  478 + e.preventDefault()
  479 + break
  480 +
  481 + case 9: // tab
  482 + case 13: // enter
  483 +
  484 + // We will handle creating tokens from autocomplete in autocomplete events
  485 + if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break
  486 +
  487 + // We will handle creating tokens from typeahead in typeahead events
  488 + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break
  489 + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break
  490 +
  491 + // Create token
  492 + if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) {
  493 + return this.createTokensFromInput(e, this.$input.data('edit'));
  494 + }
  495 +
  496 + // Edit token
  497 + if (e.keyCode === 13) {
  498 + if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break
  499 + if (!_self.options.allowEditing) break
  500 + this.edit( this.$wrapper.find('.token.active') )
  501 + }
  502 + }
  503 +
  504 + function leftRight(direction) {
  505 + if (_self.$input.is(document.activeElement)) {
  506 + if (_self.$input.val().length > 0) return
  507 +
  508 + direction += 'All'
  509 + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first')
  510 + if (!$token.length) return
  511 +
  512 + _self.preventInputFocus = true
  513 + _self.preventDeactivation = true
  514 +
  515 + _self.activate( $token )
  516 + e.preventDefault()
  517 +
  518 + } else {
  519 + _self[direction]( e.shiftKey )
  520 + e.preventDefault()
  521 + }
  522 + }
  523 +
  524 + function upDown(direction) {
  525 + if (!e.shiftKey) return
  526 +
  527 + if (_self.$input.is(document.activeElement)) {
  528 + if (_self.$input.val().length > 0) return
  529 +
  530 + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first')
  531 + if (!$token.length) return
  532 +
  533 + _self.activate( $token )
  534 + }
  535 +
  536 + var opposite = direction === 'prev' ? 'next' : 'prev'
  537 + , position = direction === 'prev' ? 'first' : 'last'
  538 +
  539 + _self.$firstActiveToken[opposite + 'All']('.token').each(function() {
  540 + _self.deactivate( $(this) )
  541 + })
  542 +
  543 + _self.activate( _self.$wrapper.find('.token:' + position), true, true )
  544 + e.preventDefault()
  545 + }
  546 +
  547 + this.lastKeyDown = e.keyCode
  548 + }
  549 +
  550 + , keypress: function(e) {
  551 +
  552 + // Comma
  553 + if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) {
  554 + if (this.$input.val()) {
  555 + this.createTokensFromInput(e)
  556 + }
  557 + return false;
  558 + }
  559 + }
  560 +
  561 + , keyup: function (e) {
  562 + this.preventInputFocus = false
  563 +
  564 + if (!this.focused) return
  565 +
  566 + switch(e.keyCode) {
  567 + case 8: // backspace
  568 + if (this.$input.is(document.activeElement)) {
  569 + if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break
  570 +
  571 + this.preventDeactivation = true
  572 + var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first')
  573 +
  574 + if (!$prevToken.length) break
  575 +
  576 + this.activate( $prevToken )
  577 + } else {
  578 + this.remove(e)
  579 + }
  580 + break
  581 +
  582 + case 46: // delete
  583 + this.remove(e, 'next')
  584 + break
  585 + }
  586 + this.lastKeyUp = e.keyCode
  587 + }
  588 +
  589 + , focus: function (e) {
  590 + this.focused = true
  591 + this.$wrapper.addClass('focus')
  592 +
  593 + if (this.$input.is(document.activeElement)) {
  594 + this.$wrapper.find('.active').removeClass('active')
  595 + this.$firstActiveToken = null
  596 +
  597 + if (this.options.showAutocompleteOnFocus) {
  598 + this.search()
  599 + }
  600 + }
  601 + }
  602 +
  603 + , blur: function (e) {
  604 +
  605 + this.focused = false
  606 + this.$wrapper.removeClass('focus')
  607 +
  608 + if (!this.preventDeactivation && !this.$element.is(document.activeElement)) {
  609 + this.$wrapper.find('.active').removeClass('active')
  610 + this.$firstActiveToken = null
  611 + }
  612 +
  613 + if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) {
  614 + this.createTokensFromInput(e)
  615 + }
  616 +
  617 + this.preventDeactivation = false
  618 + this.preventCreateTokens = false
  619 + }
  620 +
  621 + , paste: function (e) {
  622 + var _self = this
  623 +
  624 + // Add tokens to existing ones
  625 + if (_self.options.allowPasting) {
  626 + setTimeout(function () {
  627 + _self.createTokensFromInput(e)
  628 + }, 1)
  629 + }
  630 + }
  631 +
  632 + , change: function (e) {
  633 + if ( e.initiator === 'tokenfield' ) return // Prevent loops
  634 +
  635 + this.setTokens( this.$element.val() )
  636 + }
  637 +
  638 + , createTokensFromInput: function (e, focus) {
  639 + if (this.$input.val().length < this.options.minLength)
  640 + return // No input, simply return
  641 +
  642 + var tokensBefore = this.getTokensList()
  643 + this.setTokens( this.$input.val(), true )
  644 +
  645 + if (tokensBefore == this.getTokensList() && this.$input.val().length)
  646 + return false // No tokens were added, do nothing (prevent form submit)
  647 +
  648 + if (this.$input.hasClass('tt-input')) {
  649 + // Typeahead acts weird when simply setting input value to empty,
  650 + // so we set the query to empty instead
  651 + this.$input.typeahead('val', '')
  652 + } else {
  653 + this.$input.val('')
  654 + }
  655 +
  656 + if (this.$input.data( 'edit' )) {
  657 + this.unedit(focus)
  658 + }
  659 +
  660 + return false // Prevent form being submitted
  661 + }
  662 +
  663 + , next: function (add) {
  664 + if (add) {
  665 + var $firstActiveToken = this.$wrapper.find('.active:first')
  666 + , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false
  667 +
  668 + if (deactivate) return this.deactivate( $firstActiveToken )
  669 + }
  670 +
  671 + var $lastActiveToken = this.$wrapper.find('.active:last')
  672 + , $nextToken = $lastActiveToken.nextAll('.token:first')
  673 +
  674 + if (!$nextToken.length) {
  675 + this.$input.focus()
  676 + return
  677 + }
  678 +
  679 + this.activate($nextToken, add)
  680 + }
  681 +
  682 + , prev: function (add) {
  683 +
  684 + if (add) {
  685 + var $lastActiveToken = this.$wrapper.find('.active:last')
  686 + , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false
  687 +
  688 + if (deactivate) return this.deactivate( $lastActiveToken )
  689 + }
  690 +
  691 + var $firstActiveToken = this.$wrapper.find('.active:first')
  692 + , $prevToken = $firstActiveToken.prevAll('.token:first')
  693 +
  694 + if (!$prevToken.length) {
  695 + $prevToken = this.$wrapper.find('.token:first')
  696 + }
  697 +
  698 + if (!$prevToken.length && !add) {
  699 + this.$input.focus()
  700 + return
  701 + }
  702 +
  703 + this.activate( $prevToken, add )
  704 + }
  705 +
  706 + , activate: function ($token, add, multi, remember) {
  707 +
  708 + if (!$token) return
  709 +
  710 + if (typeof remember === 'undefined') var remember = true
  711 +
  712 + if (multi) var add = true
  713 +
  714 + this.$copyHelper.focus()
  715 +
  716 + if (!add) {
  717 + this.$wrapper.find('.active').removeClass('active')
  718 + if (remember) {
  719 + this.$firstActiveToken = $token
  720 + } else {
  721 + delete this.$firstActiveToken
  722 + }
  723 + }
  724 +
  725 + if (multi && this.$firstActiveToken) {
  726 + // Determine first active token and the current tokens indicies
  727 + // Account for the 1 hidden textarea by subtracting 1 from both
  728 + var i = this.$firstActiveToken.index() - 2
  729 + , a = $token.index() - 2
  730 + , _self = this
  731 +
  732 + this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() {
  733 + _self.activate( $(this), true )
  734 + })
  735 + }
  736 +
  737 + $token.addClass('active')
  738 + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
  739 + }
  740 +
  741 + , activateAll: function() {
  742 + var _self = this
  743 +
  744 + this.$wrapper.find('.token').each( function (i) {
  745 + _self.activate($(this), i !== 0, false, false)
  746 + })
  747 + }
  748 +
  749 + , deactivate: function($token) {
  750 + if (!$token) return
  751 +
  752 + $token.removeClass('active')
  753 + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
  754 + }
  755 +
  756 + , toggle: function($token) {
  757 + if (!$token) return
  758 +
  759 + $token.toggleClass('active')
  760 + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
  761 + }
  762 +
  763 + , edit: function ($token) {
  764 + if (!$token) return
  765 +
  766 + var attrs = $token.data('attrs')
  767 +
  768 + // Allow changing input value before editing
  769 + var options = { attrs: attrs, relatedTarget: $token.get(0) }
  770 + var editEvent = $.Event('tokenfield:edittoken', options)
  771 + this.$element.trigger( editEvent )
  772 +
  773 + // Edit event can be cancelled if default is prevented
  774 + if (editEvent.isDefaultPrevented()) return
  775 +
  776 + $token.find('.token-label').text(attrs.value)
  777 + var tokenWidth = $token.outerWidth()
  778 +
  779 + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
  780 +
  781 + $token.replaceWith( $_input )
  782 +
  783 + this.preventCreateTokens = true
  784 +
  785 + this.$input.val( attrs.value )
  786 + .select()
  787 + .data( 'edit', true )
  788 + .width( tokenWidth )
  789 +
  790 + this.update();
  791 +
  792 + // Indicate that token is now being edited, and is replaced with an input field in the DOM
  793 + this.$element.trigger($.Event('tokenfield:editedtoken', options ))
  794 + }
  795 +
  796 + , unedit: function (focus) {
  797 + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
  798 + $_input.appendTo( this.$wrapper )
  799 +
  800 + this.$input.data('edit', false)
  801 + this.$mirror.text('')
  802 +
  803 + this.update()
  804 +
  805 + // Because moving the input element around in DOM
  806 + // will cause it to lose focus, we provide an option
  807 + // to re-focus the input after appending it to the wrapper
  808 + if (focus) {
  809 + var _self = this
  810 + setTimeout(function () {
  811 + _self.$input.focus()
  812 + }, 1)
  813 + }
  814 + }
  815 +
  816 + , remove: function (e, direction) {
  817 + if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return
  818 +
  819 + var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active')
  820 +
  821 + if (e.type !== 'click') {
  822 + if (!direction) var direction = 'prev'
  823 + this[direction]()
  824 +
  825 + // Was it the first token?
  826 + if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0
  827 + }
  828 +
  829 + // Prepare events and their options
  830 + var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) }
  831 + , removeEvent = $.Event('tokenfield:removetoken', options)
  832 +
  833 + this.$element.trigger(removeEvent);
  834 +
  835 + // Remove event can be intercepted and cancelled
  836 + if (removeEvent.isDefaultPrevented()) return
  837 +
  838 + var removedEvent = $.Event('tokenfield:removedtoken', options)
  839 + , changeEvent = $.Event('change', { initiator: 'tokenfield' })
  840 +
  841 + // Remove token from DOM
  842 + $token.remove()
  843 +
  844 + // Trigger events
  845 + this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent )
  846 +
  847 + // Focus, when necessary:
  848 + // When there are no more tokens, or if this was the first token
  849 + // and it was removed with backspace or it was clicked on
  850 + if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus()
  851 +
  852 + // Adjust input width
  853 + this.$input.css('width', this.options.minWidth + 'px')
  854 + this.update()
  855 +
  856 + // Cancel original event handlers
  857 + e.preventDefault()
  858 + e.stopPropagation()
  859 + }
  860 +
  861 + /**
  862 + * Update tokenfield dimensions
  863 + */
  864 + , update: function (e) {
  865 + var value = this.$input.val()
  866 + , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10)
  867 + , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10)
  868 + , inputPadding = inputPaddingLeft + inputPaddingRight
  869 +
  870 + if (this.$input.data('edit')) {
  871 +
  872 + if (!value) {
  873 + value = this.$input.prop("placeholder")
  874 + }
  875 + if (value === this.$mirror.text()) return
  876 +
  877 + this.$mirror.text(value)
  878 +
  879 + var mirrorWidth = this.$mirror.width() + 10;
  880 + if ( mirrorWidth > this.$wrapper.width() ) {
  881 + return this.$input.width( this.$wrapper.width() )
  882 + }
  883 +
  884 + this.$input.width( mirrorWidth )
  885 + }
  886 + else {
  887 + var w = (this.textDirection === 'rtl')
  888 + ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1
  889 + : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding;
  890 + //
  891 + // some usecases pre-render widget before attaching to DOM,
  892 + // dimensions returned by jquery will be NaN -> we default to 100%
  893 + // so placeholder won't be cut off.
  894 + isNaN(w) ? this.$input.width('100%') : this.$input.width(w);
  895 + }
  896 + }
  897 +
  898 + , focusInput: function (e) {
  899 + if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return
  900 + // Focus only after the current call stack has cleared,
  901 + // otherwise has no effect.
  902 + // Reason: mousedown is too early - input will lose focus
  903 + // after mousedown. However, since the input may be moved
  904 + // in DOM, there may be no click or mouseup event triggered.
  905 + var _self = this
  906 + setTimeout(function() {
  907 + _self.$input.focus()
  908 + }, 0)
  909 + }
  910 +
  911 + , search: function () {
  912 + if ( this.$input.data('ui-autocomplete') ) {
  913 + this.$input.autocomplete('search')
  914 + }
  915 + }
  916 +
  917 + , disable: function () {
  918 + this.setProperty('disabled', true);
  919 + }
  920 +
  921 + , enable: function () {
  922 + this.setProperty('disabled', false);
  923 + }
  924 +
  925 + , readonly: function () {
  926 + this.setProperty('readonly', true);
  927 + }
  928 +
  929 + , writeable: function () {
  930 + this.setProperty('readonly', false);
  931 + }
  932 +
  933 + , setProperty: function(property, value) {
  934 + this['_' + property] = value;
  935 + this.$input.prop(property, value);
  936 + this.$element.prop(property, value);
  937 + this.$wrapper[ value ? 'addClass' : 'removeClass' ](property);
  938 + }
  939 +
  940 + , destroy: function() {
  941 + // Set field value
  942 + this.$element.val( this.getTokensList() );
  943 + // Restore styles and properties
  944 + this.$element.css( this.$element.data('original-styles') );
  945 + this.$element.prop( 'tabindex', this.$element.data('original-tabindex') );
  946 +
  947 + // Re-route tokenfield label to original input
  948 + var $label = $( 'label[for="' + this.$input.prop('id') + '"]' )
  949 + if ( $label.length ) {
  950 + $label.prop( 'for', this.$element.prop('id') )
  951 + }
  952 +
  953 + // Move original element outside of tokenfield wrapper
  954 + this.$element.insertBefore( this.$wrapper );
  955 +
  956 + // Remove tokenfield-related data
  957 + this.$element.removeData('original-styles')
  958 + .removeData('original-tabindex')
  959 + .removeData('bs.tokenfield');
  960 +
  961 + // Remove tokenfield from DOM
  962 + this.$wrapper.remove();
  963 + this.$mirror.remove();
  964 +
  965 + var $_element = this.$element;
  966 +
  967 + return $_element;
  968 + }
  969 +
  970 + }
  971 +
  972 +
  973 + /* TOKENFIELD PLUGIN DEFINITION
  974 + * ======================== */
  975 +
  976 + var old = $.fn.tokenfield
  977 +
  978 + $.fn.tokenfield = function (option, param) {
  979 + var value
  980 + , args = []
  981 +
  982 + Array.prototype.push.apply( args, arguments );
  983 +
  984 + var elements = this.each(function () {
  985 + var $this = $(this)
  986 + , data = $this.data('bs.tokenfield')
  987 + , options = typeof option == 'object' && option
  988 +
  989 + if (typeof option === 'string' && data && data[option]) {
  990 + args.shift()
  991 + value = data[option].apply(data, args)
  992 + } else {
  993 + if (!data && typeof option !== 'string' && !param) {
  994 + $this.data('bs.tokenfield', (data = new Tokenfield(this, options)))
  995 + $this.trigger('tokenfield:initialize')
  996 + }
  997 + }
  998 + })
  999 +
  1000 + return typeof value !== 'undefined' ? value : elements;
  1001 + }
  1002 +
  1003 + $.fn.tokenfield.defaults = {
  1004 + minWidth: 60,
  1005 + minLength: 0,
  1006 + html: true,
  1007 + allowEditing: true,
  1008 + allowPasting: true,
  1009 + limit: 0,
  1010 + autocomplete: {},
  1011 + typeahead: {},
  1012 + showAutocompleteOnFocus: false,
  1013 + createTokensOnBlur: false,
  1014 + delimiter: ',',
  1015 + beautify: true,
  1016 + inputType: 'text'
  1017 + }
  1018 +
  1019 + $.fn.tokenfield.Constructor = Tokenfield
  1020 +
  1021 +
  1022 + /* TOKENFIELD NO CONFLICT
  1023 + * ================== */
  1024 +
  1025 + $.fn.tokenfield.noConflict = function () {
  1026 + $.fn.tokenfield = old
  1027 + return this
  1028 + }
  1029 +
  1030 + return Tokenfield;
  1031 +
  1032 +}));
... ...
plugins/fb_app/public/javascripts/fb_app.js 0 → 100644
... ... @@ -0,0 +1,312 @@
  1 +fb_app = {
  2 + current_url: '',
  3 +
  4 + locales: {
  5 +
  6 + },
  7 +
  8 + config: {
  9 + url_prefix: '',
  10 + save_auth_url: '',
  11 + show_login_url: '',
  12 +
  13 + init: function() {
  14 +
  15 + },
  16 +
  17 + },
  18 +
  19 + timeline: {
  20 + appId: '',
  21 + app_scope: 'publish_actions',
  22 +
  23 + loading: function() {
  24 + jQuery('#fb-app-connect-status').empty().addClass('loading').height(150)
  25 + },
  26 +
  27 + connect: function() {
  28 + this.loading();
  29 + fb_app.fb.scope = this.app_scope
  30 + fb_app.fb.connect(function (response) {
  31 + fb_app.auth.receive(response)
  32 + });
  33 + },
  34 +
  35 + disconnect: function() {
  36 + // 'not_authorized' is used to disconnect from facebook
  37 + jQuery('#fb-app-modal-wrap #fb-app-modal-intro').html(
  38 + fb_app.locales.confirm_disconnect
  39 + )
  40 + jQuery('#fb-app-modal-wrap .modal-button-no')
  41 + .html(fb_app.locales.cancel_button)
  42 + .attr('onClick', 'noosfero.modal.close(); return false')
  43 + jQuery('#fb-app-modal-wrap .modal-button-yes')
  44 + .html(fb_app.locales.confirm_disconnect_button)
  45 + .attr('onClick', 'fb_app.timeline.disconnect_confirmed();noosfero.modal.close(); return false')
  46 + noosfero.modal.html(jQuery('#fb-app-modal-wrap').html())
  47 + },
  48 +
  49 + disconnect_confirmed: function() {
  50 + this.loading();
  51 + fb_app.auth.receive({status: 'not_authorized'})
  52 + },
  53 +
  54 + connect_to_another: function() {
  55 + this.disconnect();
  56 + fb_app.fb.connect_to_another(this.connect)
  57 + },
  58 + },
  59 +
  60 + page_tab: {
  61 + appId: '',
  62 + nextUrl: '',
  63 +
  64 + init: function() {
  65 + FB.Canvas.scrollTo(0,140);
  66 + // While Braulio doesnt make the catalog-options-bar work in a product page, I'll hide it. User will have to go back to the catalog to see this bar, provisorily...
  67 + jQuery('#product-page').prev('form').hide();
  68 + },
  69 +
  70 + config: {
  71 +
  72 + init: function() {
  73 + this.change_type($('select#page_tab_config_type'))
  74 +
  75 + },
  76 +
  77 + edit: function(button) {
  78 + var page_tab = button.parents('.page-tab')
  79 + page_tab.find('form').toggle(400)
  80 + },
  81 +
  82 + remove: function(button, url) {
  83 + var page_tab = button.parents('.page-tab')
  84 + var name = page_tab.find('#page_tab_name').val()
  85 + //jQuery('#fb-app-modal-catalog-name').text(name)
  86 + jQuery('#fb-app-modal-wrap #fb-app-modal-intro').html(
  87 + fb_app.locales.confirm_removal
  88 + )
  89 + jQuery('#fb-app-modal-wrap .modal-button-no')
  90 + .html(fb_app.locales.cancel_button)
  91 + .attr('onClick', 'noosfero.modal.close(); return false')
  92 + jQuery('#fb-app-modal-wrap .modal-button-yes')
  93 + .html(fb_app.locales.confirm_removal_button)
  94 + .attr('onClick', 'fb_app.page_tab.config.remove_confirmed(this);noosfero.modal.close(); return false')
  95 + .attr('target_url',url)
  96 + .attr('target_id','#'+page_tab.attr('id'))
  97 + noosfero.modal.html(jQuery('#fb-app-modal-wrap').html())
  98 + },
  99 +
  100 + remove_confirmed: function(el) {
  101 + el = jQuery(el)
  102 + jQuery.post(el.attr('target_url'), function() {
  103 + var page_tab = jQuery(el.attr('target_id'))
  104 + page_tab.remove()
  105 + })
  106 + },
  107 +
  108 + close: function(pageId) {
  109 + noosfero.modal.close()
  110 + jQuery('#content').html('').addClass('loading')
  111 + fb_app.fb.redirect_to_tab(pageId, fb_app.page_tab.appId)
  112 + },
  113 +
  114 + validate: function(form) {
  115 + for (var i=0; tinymce.editors[i]; i++) {
  116 + var editor = tinymce.editors[i]
  117 + var textarea = editor.getElement()
  118 + textarea.value = editor.getContent()
  119 + }
  120 +
  121 + if (form.find('#page_tab_title').val().trim()=='') {
  122 + noosfero.modal.html('<div id="fb-app-error">'+fb_app.locales.error_empty_title+'</div>')
  123 + return false
  124 + } else {
  125 + var selected_type = form.find('#page_tab_config_type').val()
  126 + var sub_option = form.find('.config-type-'+selected_type+' input')
  127 + if (sub_option.length > 0 && sub_option.val().trim()=='') {
  128 + noosfero.modal.html('<div id="fb-app-error">'+fb_app.locales.error_empty_settings+'</div>')
  129 + return false
  130 + }
  131 + }
  132 + return true
  133 + },
  134 +
  135 + add: function (form) {
  136 + if (!this.validate(form))
  137 + return false
  138 + // this checks if the user is using FB as a page and offer a switch
  139 + FB.login(function(response) {
  140 + if (response.status != 'connected') return
  141 + var nextUrl = fb_app.page_tab.nextUrl + '?' + form.serialize()
  142 + window.location.href = fb_app.fb.add_tab_url(fb_app.page_tab.appId, nextUrl)
  143 + })
  144 + return false
  145 + },
  146 +
  147 + save: function(form) {
  148 + if (!this.validate(form))
  149 + return false
  150 + jQuery(form).ajaxSubmit({
  151 + dataType: 'script',
  152 + })
  153 + return false
  154 + },
  155 +
  156 + change_type: function(select) {
  157 + select = jQuery(select)
  158 + var page_tab = select.parents('.page-tab')
  159 + var config_selector = '.config-type-'+select.val()
  160 + var config = page_tab.find(config_selector)
  161 + var to_show = config
  162 + var to_hide = page_tab.find('.config-type:not('+config_selector+')')
  163 +
  164 + to_show.show().
  165 + find('input').prop('disabled', false)
  166 + to_show.find('.tokenfield').removeClass('disabled')
  167 + to_hide.hide().
  168 + find('input').prop('disabled', true)
  169 + },
  170 +
  171 + profile: {
  172 +
  173 + onchange: function(input) {
  174 + if (input.val())
  175 + input.removeAttr('placeholder')
  176 + else
  177 + input.attr('placeholder', input.attr('data-placeholder'))
  178 + },
  179 + },
  180 + },
  181 +
  182 + },
  183 +
  184 + auth: {
  185 + status: 'not_authorized',
  186 +
  187 + load: function (html) {
  188 + jQuery('#fb-app-settings').html(html)
  189 + },
  190 + loadLogin: function (html) {
  191 + if (this.status == 'not_authorized')
  192 + jQuery('#fb-app-connect').html(html).removeClass('loading')
  193 + else
  194 + jQuery('#fb-app-login').html(html)
  195 + },
  196 +
  197 + receive: function(response) {
  198 + fb_app.fb.authResponse = response
  199 + fb_app.auth.save(response)
  200 + jQuery('html,body').animate({ scrollTop: jQuery('#fb-app-settings').offset().top-100 }, 400)
  201 + },
  202 +
  203 + transformParams: function(response) {
  204 + var authResponse = response.authResponse
  205 + if (!authResponse)
  206 + return {auth: {status: response.status}}
  207 + else
  208 + return {
  209 + auth: {
  210 + status: response.status,
  211 + access_token: authResponse.accessToken,
  212 + expires_in: authResponse.expiresIn,
  213 + signed_request: authResponse.signedRequest,
  214 + provider_user_id: authResponse.userID,
  215 + }
  216 + }
  217 + },
  218 +
  219 + showLogin: function(response) {
  220 + jQuery.get(fb_app.config.show_login_url, this.transformParams(response), this.loadLogin)
  221 + },
  222 +
  223 + save: function(response) {
  224 + jQuery.post(fb_app.config.save_auth_url, this.transformParams(response), this.load)
  225 + },
  226 + },
  227 +
  228 +
  229 + // interface to facebook's SDK
  230 + fb: {
  231 + appId: '',
  232 + scope: '',
  233 + inited: false,
  234 + initCode: null,
  235 +
  236 + prepareAsyncInit: function(appId, asyncInitCode) {
  237 + this.id = appId
  238 + this.initCode = asyncInitCode
  239 +
  240 + window.fbAsyncInit = function() {
  241 + FB.init({
  242 + appId: appId,
  243 + cookie: true,
  244 + xfbml: true,
  245 + status: true,
  246 + })
  247 +
  248 + // automatic iframe's resize
  249 + // FIXME: move to page tab embed code
  250 + fb_app.fb.size_change()
  251 + jQuery(document).on('DOMNodeInserted', fb_app.fb.size_change)
  252 +
  253 + if (asyncInitCode)
  254 + jQuery.globalEval(asyncInitCode)
  255 +
  256 + fb_app.fb.inited = true
  257 + }
  258 + },
  259 +
  260 + init: function() {
  261 + // the SDK is loaded on views/fb_app_plugin/_load.html.slim and then call window.fbAsyncInit
  262 + },
  263 +
  264 + size_change: function() {
  265 + FB.Canvas.setSize({height: jQuery('body').height()+100})
  266 + },
  267 +
  268 + redirect_to_tab: function(pageId, appId) {
  269 + window.location.href = 'https://facebook.com/' + pageId + '?sk=app_' + appId
  270 + },
  271 +
  272 + add_tab_url: function (appId, nextUrl) {
  273 + return 'https://www.facebook.com/dialog/pagetab?' + jQuery.param({app_id: appId, next: nextUrl})
  274 + },
  275 +
  276 + connect: function(callback) {
  277 + FB.login(function(response) {
  278 + if (callback) callback(response)
  279 + }, {scope: fb_app.fb.scope})
  280 + },
  281 +
  282 + connect_to_another: function(callback) {
  283 + this.logout(this.connect(callback))
  284 + },
  285 +
  286 + logout: function(callback) {
  287 + // this checks if the user is using FB as a page and offer a switch
  288 + FB.login(function(response) {
  289 + FB.logout(function(response) {
  290 + if (callback) callback(response)
  291 + })
  292 + })
  293 + },
  294 +
  295 + // not to be used
  296 + delete: function(callback) {
  297 + FB.api("/me/permissions", "DELETE", function(response) {
  298 + if (callback) callback(response)
  299 + })
  300 + },
  301 +
  302 + checkLoginStatus: function() {
  303 + FB.getLoginStatus(function(response) {
  304 + // don't do nothing, this is just to fetch auth after init
  305 + })
  306 + },
  307 +
  308 + },
  309 +
  310 +}
  311 +
  312 +
... ...
plugins/fb_app/public/style.scss 0 → 120000
... ... @@ -0,0 +1 @@
  1 +stylesheets/style.scss
0 2 \ No newline at end of file
... ...
plugins/fb_app/public/stylesheets/_base.scss 0 → 100644
... ... @@ -0,0 +1,214 @@
  1 +/* use with @extend, CSS clear bugfix */
  2 +.clean {
  3 + clear: both;
  4 +}
  5 +.container-clean {
  6 + overflow: hidden;
  7 + display: inline-block; /* Necessary to trigger "hasLayout" in IE */
  8 + display: block; /* Sets element back to block */
  9 +}
  10 +
  11 +/* layout base parameters */
  12 +$modules: 12;
  13 +$base: 8px;
  14 +$wireframe: 1040px;
  15 +
  16 +/* heights should only use multiples of this */
  17 +$height: $base;
  18 +
  19 +/* base measurements */
  20 +$intercolumn: 2*$base;
  21 +$module: $wireframe/$modules - $intercolumn;
  22 +
  23 +/* widths should only use one of these */
  24 +$module01: 01*$module + 00*$intercolumn;
  25 +$module02: 02*$module + 01*$intercolumn;
  26 +$module03: 03*$module + 02*$intercolumn;
  27 +$module04: 04*$module + 03*$intercolumn;
  28 +$module05: 05*$module + 04*$intercolumn;
  29 +$module06: 06*$module + 05*$intercolumn;
  30 +$module07: 07*$module + 06*$intercolumn;
  31 +$module08: 08*$module + 07*$intercolumn;
  32 +$module09: 09*$module + 08*$intercolumn;
  33 +$module09: 09*$module + 08*$intercolumn;
  34 +$module10: 10*$module + 09*$intercolumn;
  35 +$module11: 11*$module + 10*$intercolumn;
  36 +$module12: 12*$module + 11*$intercolumn;
  37 +$module01p: percentage($module01/$wireframe);
  38 +$module02p: percentage($module02/$wireframe);
  39 +$module03p: percentage($module03/$wireframe);
  40 +$module04p: percentage($module04/$wireframe);
  41 +$module05p: percentage($module05/$wireframe);
  42 +$module06p: percentage($module06/$wireframe);
  43 +$module07p: percentage($module07/$wireframe);
  44 +$module08p: percentage($module08/$wireframe);
  45 +$module09p: percentage($module09/$wireframe);
  46 +$module10p: percentage($module10/$wireframe);
  47 +$module11p: percentage($module11/$wireframe);
  48 +$module12p: percentage($module12/$wireframe);
  49 +
  50 +/* paddings and margins should only use one of these
  51 + Ps. 1: disccount the borders size from padding, as borders uses padding's space.
  52 + Ps. 2: because of W3C's content-box default box sizing, padding sums to width size. If your
  53 + box doesn't have a padding, then sum $intercolumn to the width.
  54 + */
  55 +$margin: $intercolumn;
  56 +$half-margin: $margin/2;
  57 +$padding: $intercolumn/2;
  58 +$half-padding: $padding/2;
  59 +$marginp: percentage($margin/$wireframe);
  60 +$half-marginp: percentage($half-margin/$wireframe);
  61 +$paddingp: percentage($padding/$wireframe);
  62 +$half-paddingp: percentage($half-padding/$wireframe);
  63 +
  64 +$wireframe-padding: 5*$padding;
  65 +
  66 +/* use for borders */
  67 +$border: 1px;
  68 +$border-radius: 5px;
  69 +
  70 +/* use for text shadows */
  71 +$shadow: 2px;
  72 +
  73 +/* Colors */
  74 +
  75 +$border-action-button: #F4A439;
  76 +$bg-action-button: #FBCA47;
  77 +$bg-selection-button: white;
  78 +
  79 +/* Fonts */
  80 +
  81 +/* Paragraphs Styles (use with @extend) */
  82 +
  83 +.pstyle-none {
  84 + font-size: 12px;
  85 +}
  86 +.pstyle-basic {
  87 + font-size: 16px;
  88 +}
  89 +.pstyle-button {
  90 + font-size: 16px;
  91 +}
  92 +.pstyle-button-small {
  93 + font-size: 13px;
  94 +}
  95 +.pstyle-title {
  96 + font-size: 72px;
  97 +}
  98 +.pstyle-h1 {
  99 + font-size: 34px;
  100 +}
  101 +.pstyle-h2 {
  102 + font-size: 26px;
  103 +}
  104 +.pstyle-h3 {
  105 + font-size: 21px;
  106 +}
  107 +.pstyle-h4 {
  108 + font-size: 16px;
  109 +}
  110 +.pstyle-h5 {
  111 + font-size: 13px;
  112 +}
  113 +.pstyle-title-section {
  114 + font-size: 92px;
  115 +}
  116 +.pstyle-field {
  117 + font-size: 13px;
  118 +}
  119 +.pstyle-menu-big-selected {
  120 + font-size: 21px;
  121 +}
  122 +.pstyle-menu-big-unselected {
  123 + font-size: 21px;
  124 +}
  125 +.pstyle-menu-medium-selected {
  126 + font-size: 16px;
  127 +}
  128 +.pstyle-menu-medium-unselected {
  129 + font-size: 16px;
  130 +}
  131 +.pstyle-menu-small-selected {
  132 + font-size: 13px;
  133 +}
  134 +.pstyle-menu-small-unselected {
  135 + font-size: 13px;
  136 +}
  137 +.pstyle-tp4 {
  138 + font-size: 42px;
  139 +}
  140 +.pstyle-tp3 {
  141 + font-size: 34px;
  142 +}
  143 +.pstyle-tp2 {
  144 + font-size: 26px;
  145 +}
  146 +.pstyle-tp1 {
  147 + font-size: 21px;
  148 +}
  149 +.pstyle-tm1 {
  150 + font-size: 13px;
  151 +}
  152 +.pstyle-tm2 {
  153 + font-size: 10px;
  154 +}
  155 +.subtitle {
  156 + @extend .pstyle-tm2;
  157 +}
  158 +
  159 +/* Images */
  160 +
  161 +$profile-thumb-size: 4*$base;
  162 +$profile-portrait-size: 10*$base;
  163 +
  164 +/* profile-image that can be centered and resized with aspect ratio */
  165 +.profile-image {
  166 + display: inline-block;
  167 +
  168 + &.thumb {
  169 + width: $profile-thumb-size;
  170 + height: $profile-thumb-size;
  171 + }
  172 + &.portrait {
  173 + width: $profile-portrait-size;
  174 + height: $profile-portrait-size;
  175 + }
  176 +
  177 + /* do not put padding in this as background size will consider it. */
  178 + .inner {
  179 + display: block;
  180 + width: 100%;
  181 + height: 100%;
  182 + background-repeat: no-repeat;
  183 + background-position: center;
  184 + background-size: 100%;
  185 + background-size: contain; /* css3 enabled */
  186 + }
  187 +}
  188 +
  189 +/* Buttons */
  190 +
  191 +.action-button {
  192 + display: inline-block;
  193 + padding: $half-padding $padding;
  194 + height: auto;
  195 + width: auto;
  196 + //&:visited, &:active, &:hover { color: white; }
  197 + background: $bg-action-button;
  198 + border: $border solid $border-action-button;
  199 + cursor: pointer;
  200 + color: black;
  201 + font-weight: bold;
  202 + line-height: 2*$height;
  203 + text-align: center;
  204 + text-decoration: none;
  205 + text-transform: uppercase;
  206 + text-shadow: none;
  207 + border-radius: $border-radius;
  208 +}
  209 +
  210 +.selection-button {
  211 + @extend .action-button;
  212 + background: $bg-selection-button;
  213 +}
  214 +
... ...
plugins/fb_app/public/stylesheets/bootstrap-tokenfield.css 0 → 100644
... ... @@ -0,0 +1,209 @@
  1 +/*!
  2 + * bootstrap-tokenfield
  3 + * https://github.com/sliptree/bootstrap-tokenfield
  4 + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
  5 + */
  6 +@-webkit-keyframes blink {
  7 + 0% {
  8 + border-color: #ededed;
  9 + }
  10 + 100% {
  11 + border-color: #b94a48;
  12 + }
  13 +}
  14 +@-moz-keyframes blink {
  15 + 0% {
  16 + border-color: #ededed;
  17 + }
  18 + 100% {
  19 + border-color: #b94a48;
  20 + }
  21 +}
  22 +@keyframes blink {
  23 + 0% {
  24 + border-color: #ededed;
  25 + }
  26 + 100% {
  27 + border-color: #b94a48;
  28 + }
  29 +}
  30 +.tokenfield {
  31 + height: auto;
  32 + min-height: 34px;
  33 + padding-bottom: 0px;
  34 +}
  35 +.tokenfield.focus {
  36 + border-color: #66afe9;
  37 + outline: 0;
  38 + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
  39 + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
  40 +}
  41 +.tokenfield .token {
  42 + -webkit-box-sizing: border-box;
  43 + -moz-box-sizing: border-box;
  44 + box-sizing: border-box;
  45 + -webkit-border-radius: 3px;
  46 + -moz-border-radius: 3px;
  47 + border-radius: 3px;
  48 + display: inline-block;
  49 + border: 1px solid #d9d9d9;
  50 + background-color: #ededed;
  51 + white-space: nowrap;
  52 + margin: -1px 5px 5px 0;
  53 + height: 22px;
  54 + vertical-align: top;
  55 + cursor: default;
  56 +}
  57 +.tokenfield .token:hover {
  58 + border-color: #b9b9b9;
  59 +}
  60 +.tokenfield .token.active {
  61 + border-color: #52a8ec;
  62 + border-color: rgba(82, 168, 236, 0.8);
  63 +}
  64 +.tokenfield .token.duplicate {
  65 + border-color: #ebccd1;
  66 + -webkit-animation-name: blink;
  67 + animation-name: blink;
  68 + -webkit-animation-duration: 0.1s;
  69 + animation-duration: 0.1s;
  70 + -webkit-animation-direction: normal;
  71 + animation-direction: normal;
  72 + -webkit-animation-timing-function: ease;
  73 + animation-timing-function: ease;
  74 + -webkit-animation-iteration-count: infinite;
  75 + animation-iteration-count: infinite;
  76 +}
  77 +.tokenfield .token.invalid {
  78 + background: none;
  79 + border: 1px solid transparent;
  80 + -webkit-border-radius: 0;
  81 + -moz-border-radius: 0;
  82 + border-radius: 0;
  83 + border-bottom: 1px dotted #d9534f;
  84 +}
  85 +.tokenfield .token.invalid.active {
  86 + background: #ededed;
  87 + border: 1px solid #ededed;
  88 + -webkit-border-radius: 3px;
  89 + -moz-border-radius: 3px;
  90 + border-radius: 3px;
  91 +}
  92 +.tokenfield .token .token-label {
  93 + display: inline-block;
  94 + overflow: hidden;
  95 + text-overflow: ellipsis;
  96 + padding-left: 4px;
  97 + vertical-align: top;
  98 +}
  99 +.tokenfield .token .close {
  100 + font-family: Arial;
  101 + display: inline-block;
  102 + line-height: 100%;
  103 + font-size: 1.1em;
  104 + line-height: 1.49em;
  105 + margin-left: 5px;
  106 + float: none;
  107 + height: 100%;
  108 + vertical-align: top;
  109 + padding-right: 4px;
  110 +}
  111 +.tokenfield .token-input {
  112 + background: none;
  113 + width: 60px;
  114 + min-width: 60px;
  115 + border: 0;
  116 + height: 20px;
  117 + padding: 0;
  118 + margin-bottom: 6px;
  119 + -webkit-box-shadow: none;
  120 + box-shadow: none;
  121 +}
  122 +.tokenfield .token-input:focus {
  123 + border-color: transparent;
  124 + outline: 0;
  125 + /* IE6-9 */
  126 + -webkit-box-shadow: none;
  127 + box-shadow: none;
  128 +}
  129 +.tokenfield.disabled {
  130 + cursor: not-allowed;
  131 + background-color: #eeeeee;
  132 +}
  133 +.tokenfield.disabled .token-input {
  134 + cursor: not-allowed;
  135 +}
  136 +.tokenfield.disabled .token:hover {
  137 + cursor: not-allowed;
  138 + border-color: #d9d9d9;
  139 +}
  140 +.tokenfield.disabled .token:hover .close {
  141 + cursor: not-allowed;
  142 + opacity: 0.2;
  143 + filter: alpha(opacity=20);
  144 +}
  145 +.has-warning .tokenfield.focus {
  146 + border-color: #66512c;
  147 + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
  148 + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
  149 +}
  150 +.has-error .tokenfield.focus {
  151 + border-color: #843534;
  152 + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
  153 + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
  154 +}
  155 +.has-success .tokenfield.focus {
  156 + border-color: #2b542c;
  157 + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
  158 + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
  159 +}
  160 +.tokenfield.input-sm,
  161 +.input-group-sm .tokenfield {
  162 + min-height: 30px;
  163 + padding-bottom: 0px;
  164 +}
  165 +.input-group-sm .token,
  166 +.tokenfield.input-sm .token {
  167 + height: 20px;
  168 + margin-bottom: 4px;
  169 +}
  170 +.input-group-sm .token-input,
  171 +.tokenfield.input-sm .token-input {
  172 + height: 18px;
  173 + margin-bottom: 5px;
  174 +}
  175 +.tokenfield.input-lg,
  176 +.input-group-lg .tokenfield {
  177 + min-height: 45px;
  178 + padding-bottom: 4px;
  179 +}
  180 +.input-group-lg .token,
  181 +.tokenfield.input-lg .token {
  182 + height: 25px;
  183 +}
  184 +.input-group-lg .token-label,
  185 +.tokenfield.input-lg .token-label {
  186 + line-height: 23px;
  187 +}
  188 +.input-group-lg .token .close,
  189 +.tokenfield.input-lg .token .close {
  190 + line-height: 1.3em;
  191 +}
  192 +.input-group-lg .token-input,
  193 +.tokenfield.input-lg .token-input {
  194 + height: 23px;
  195 + line-height: 23px;
  196 + margin-bottom: 6px;
  197 + vertical-align: top;
  198 +}
  199 +.tokenfield.rtl {
  200 + direction: rtl;
  201 + text-align: right;
  202 +}
  203 +.tokenfield.rtl .token {
  204 + margin: -1px 0 5px 5px;
  205 +}
  206 +.tokenfield.rtl .token .token-label {
  207 + padding-left: 0px;
  208 + padding-right: 4px;
  209 +}
... ...
plugins/fb_app/public/stylesheets/style.scss 0 → 100644
... ... @@ -0,0 +1,238 @@
  1 +@import 'base';
  2 +
  3 +body.controller-fb_app_plugin_page_tab {
  4 +
  5 + /* Catalog Tab in Facebook */
  6 + background: #fff;
  7 +
  8 + #top-bar, #theme-footer, #product-category, #product-page .button-bar {
  9 + display: none;
  10 + }
  11 + .navbar-static-top {
  12 + border-radius: 4px;
  13 + }
  14 + #wrap-1 {
  15 + box-shadow: none;
  16 + }
  17 + .container {
  18 + width: 100%;
  19 + }
  20 + #content .no-boxes {
  21 + padding: 0;
  22 + width: 100%;
  23 + }
  24 + #content-inner {
  25 + padding-top: 0;
  26 + }
  27 + #content h1 {
  28 + margin: 0;
  29 + }
  30 + #product-list {
  31 + margin: 0 -10px;
  32 + }
  33 + #page-tab-subtitle {
  34 + margin-bottom: 15px;
  35 + background-color: rgb(241, 255, 107);
  36 + border: 1px solid #ccc;
  37 + border-top: none;
  38 + border-radius: 0px 0px 6px 6px;
  39 + font-style: italic;
  40 + padding: 10px 10px 0px;
  41 + }
  42 + #page-tab-subtitle p {
  43 + margin-bottom: 10px;
  44 + }
  45 + #product-owner {
  46 + display:block;
  47 + font-size: 120%;
  48 + font-weight: bold;
  49 + clear: both;
  50 + }
  51 + #product-list li.product {
  52 + width: 190px;
  53 + padding: 10px;
  54 + }
  55 + #product-list .product-big {
  56 + width: 160px;
  57 + }
  58 + #product-list .product-image-link {
  59 + height: 170px;
  60 + }
  61 + #product-list .expand-box {
  62 + width: 162px;
  63 + }
  64 + #theme-footer {
  65 + border: none;
  66 + }
  67 + #page-tab-footer {
  68 + font-size: 11px;
  69 + border-top: 3px solid rgb(241, 255, 107);
  70 + margin-top: 70px;
  71 + padding-top: 5px;
  72 + }
  73 + #page-tab-footer1 {
  74 + background-size: 50px;
  75 + padding-left: 50px;
  76 + }
  77 +
  78 + /* End of Catalog Tab in Facebook */
  79 +
  80 + #profile-title,
  81 + #profile-header,
  82 + #profile-theme-header,
  83 + #profile-footer,
  84 + #theme-header {
  85 + display: none;
  86 + }
  87 + .product-catalog-ctrl {
  88 + float: right;
  89 + margin-left: 3px;
  90 + }
  91 + #product-catalog-actions {
  92 + text-align: right;
  93 + }
  94 + #manage-fb-store-ctrl {
  95 + margin-bottom: 15px;
  96 + }
  97 + .modal-content {
  98 + #fb-app-page-tab-admin {
  99 + height: 400px;
  100 + }
  101 + }
  102 +
  103 +}
  104 +
  105 +body.controller-fb_app_plugin_page_tab,
  106 +body.controller-fb_app_plugin_myprofile {
  107 +
  108 + input.small-loading {
  109 + background: transparent url(/images/loading-small.gif) no-repeat scroll right center;
  110 + }
  111 +
  112 + .loading {
  113 + background: white url(/plugins/fb_app/images/loading.gif) no-repeat center center;
  114 + width: 80%;
  115 + height: 300px;
  116 + margin: auto;
  117 + }
  118 +}
  119 +
  120 +/* control panel - general */
  121 +#fb-app-modal-wrap {
  122 + display: none;
  123 +}
  124 +#fb-app-error, #fb-app-modal {
  125 + padding: 70px 30px;
  126 + font-size: 120%;
  127 +}
  128 +#fb-app-error {
  129 + color: #E44444;
  130 + font-style: italic;
  131 +}
  132 +.controller-profile_editor a.control-panel-fb-app {
  133 + background-image: url(/plugins/fb_app/images/control-panel.png);
  134 +}
  135 +#fb-app-intro {
  136 + margin: 0 40px 20px 40px;
  137 +}
  138 +#fb-app-intro-text {
  139 + border:2px #666 solid;
  140 + padding: 10px;
  141 + border-radius: 8px;
  142 +}
  143 +#fb-app-connect-status {
  144 + background-color: #eee;
  145 + border: 1px solid #ccc;
  146 + margin: 30px 0;
  147 + padding: 30px;
  148 +}
  149 +.fb-app-connection-button {
  150 + margin-top: 15px;
  151 +}
  152 +#fb-app-auth {
  153 + text-align: center;
  154 + min-height: 60px;
  155 +}
  156 +#fb-connected {
  157 + font-size: 36px;
  158 + margin: 0 30px;
  159 + color: #99f;
  160 +}
  161 +#fb-app-wrapper {
  162 + padding: 40px;
  163 + font-size: 20px;
  164 +}
  165 +#fb-app-wrapper h1 {
  166 + font-size: 39px;
  167 + margin-bottom: 50px;
  168 +}
  169 +
  170 +/* Control panel - catalog settings */
  171 +#page-tab-new {
  172 + margin-top: 30px;
  173 +}
  174 +#page-tab-new h3 {
  175 + border-top: 2px solid #ddd;
  176 + padding-top: 10px;
  177 +}
  178 +#page-tab-new h3, .edit-page-tab {
  179 + display: none;
  180 +}
  181 +.edit-page-tab {
  182 + background-color: #eee;
  183 + padding: 15px;
  184 + border-radius: 8px;
  185 +}
  186 +.edit-tab-button {
  187 + float: right;
  188 + margin-left: 10px;
  189 +}
  190 +#fb-app-timeline, #fb-app-catalogs {
  191 + border: 1px solid #999;
  192 + border-radius: 8px;
  193 + padding: 10px;
  194 +}
  195 +#content #fb-app-catalogs h3 {
  196 + font-size: 120%;
  197 + color: inherit;
  198 + margin-top: 20px;
  199 +}
  200 +#fb-app-catalogs label {
  201 + margin-top: 20px;
  202 +}
  203 +.fb-app-submit-page-tab-options {
  204 + margin-top: 20px;
  205 +}
  206 +.fb-app-final-back-button {
  207 + margin-top: 70px;
  208 +}
  209 +.tokenfield .token {
  210 + height: auto !important;
  211 +}
  212 +
  213 +@media (min-width: 768px) {
  214 + #noosfero-identity {
  215 + float: left;
  216 + }
  217 + #fb-connected {
  218 + font-size: 36px;
  219 + float:left;
  220 + margin: 0 30px;
  221 + color: #99f;
  222 + }
  223 + #fb-identity {
  224 + float: left;
  225 + }
  226 + #fb-app-timeline, #fb-app-catalogs {
  227 + width: 50%-$marginp;
  228 + float: left;
  229 + }
  230 + #fb-app-settings {
  231 + overflow: hidden;
  232 + padding-bottom: 400px;
  233 + }
  234 + #fb-app-catalogs {
  235 + margin-right: $marginp;
  236 + }
  237 +}
  238 +
... ...
plugins/fb_app/public/stylesheets/tokenfield-typeahead.css 0 → 100644
... ... @@ -0,0 +1,141 @@
  1 +/*!
  2 + * bootstrap-tokenfield
  3 + * https://github.com/sliptree/bootstrap-tokenfield
  4 + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
  5 + */
  6 +/* General Typeahead styling, from http://jsfiddle.net/ragulka/Dy9au/1/ */
  7 +.twitter-typeahead {
  8 + width: 100%;
  9 + position: relative;
  10 + vertical-align: top;
  11 +}
  12 +.twitter-typeahead .tt-input,
  13 +.twitter-typeahead .tt-hint {
  14 + margin: 0;
  15 + width: 100%;
  16 + vertical-align: middle;
  17 + background-color: #ffffff;
  18 +}
  19 +.twitter-typeahead .tt-hint {
  20 + color: #999999;
  21 + z-index: 1;
  22 + border: 1px solid transparent;
  23 +}
  24 +.twitter-typeahead .tt-input {
  25 + color: #555555;
  26 + z-index: 2;
  27 +}
  28 +.twitter-typeahead .tt-input,
  29 +.twitter-typeahead .tt-hint {
  30 + height: 34px;
  31 + padding: 6px 12px;
  32 + font-size: 14px;
  33 + line-height: 1.428571429;
  34 +}
  35 +.twitter-typeahead .input-sm.tt-input,
  36 +.twitter-typeahead .hint-sm.tt-hint {
  37 + border-radius: 3px;
  38 +}
  39 +.twitter-typeahead .input-lg.tt-input,
  40 +.twitter-typeahead .hint-lg.tt-hint {
  41 + border-radius: 6px;
  42 +}
  43 +.input-group .twitter-typeahead:first-child .tt-input,
  44 +.input-group .twitter-typeahead:first-child .tt-hint {
  45 + border-radius: 4px 0 0 4px !important;
  46 +}
  47 +.input-group .twitter-typeahead:last-child .tt-input,
  48 +.input-group .twitter-typeahead:last-child .tt-hint {
  49 + border-radius: 0 4px 4px 0 !important;
  50 +}
  51 +.input-group.input-group-sm .twitter-typeahead:first-child .tt-input,
  52 +.input-group.input-group-sm .twitter-typeahead:first-child .tt-hint {
  53 + border-radius: 3px 0 0 3px !important;
  54 +}
  55 +.input-group.input-group-sm .twitter-typeahead:last-child .tt-input,
  56 +.input-group.input-group-sm .twitter-typeahead:last-child .tt-hint {
  57 + border-radius: 0 3px 3px 0 !important;
  58 +}
  59 +.input-sm.tt-input,
  60 +.hint-sm.tt-hint,
  61 +.input-group.input-group-sm .tt-input,
  62 +.input-group.input-group-sm .tt-hint {
  63 + height: 30px;
  64 + padding: 5px 10px;
  65 + font-size: 12px;
  66 + line-height: 1.5;
  67 +}
  68 +.input-group.input-group-lg .twitter-typeahead:first-child .tt-input,
  69 +.input-group.input-group-lg .twitter-typeahead:first-child .tt-hint {
  70 + border-radius: 6px 0 0 6px !important;
  71 +}
  72 +.input-group.input-group-lg .twitter-typeahead:last-child .tt-input,
  73 +.input-group.input-group-lg .twitter-typeahead:last-child .tt-hint {
  74 + border-radius: 0 6px 6px 0 !important;
  75 +}
  76 +.input-lg.tt-input,
  77 +.hint-lg.tt-hint,
  78 +.input-group.input-group-lg .tt-input,
  79 +.input-group.input-group-lg .tt-hint {
  80 + height: 45px;
  81 + padding: 10px 16px;
  82 + font-size: 18px;
  83 + line-height: 1.33;
  84 +}
  85 +.tt-dropdown-menu {
  86 + width: 100%;
  87 + min-width: 160px;
  88 + margin-top: 2px;
  89 + padding: 5px 0;
  90 + background-color: #ffffff;
  91 + border: 1px solid #ccc;
  92 + border: 1px solid rgba(0, 0, 0, 0.15);
  93 + *border-right-width: 2px;
  94 + *border-bottom-width: 2px;
  95 + border-radius: 6px;
  96 + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
  97 + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
  98 + -webkit-background-clip: padding-box;
  99 + -moz-background-clip: padding;
  100 + background-clip: padding-box;
  101 +}
  102 +.tt-suggestion {
  103 + display: block;
  104 + padding: 3px 20px;
  105 +}
  106 +.tt-suggestion.tt-cursor {
  107 + color: #262626;
  108 + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
  109 + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
  110 + background-repeat: repeat-x;
  111 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
  112 +}
  113 +.tt-suggestion.tt-cursor a {
  114 + color: #ffffff;
  115 +}
  116 +.tt-suggestion p {
  117 + margin: 0;
  118 +}
  119 +/* Tokenfield-specific Typeahead styling */
  120 +.tokenfield .twitter-typeahead {
  121 + width: auto;
  122 +}
  123 +.tokenfield .twitter-typeahead .tt-hint {
  124 + padding: 0;
  125 + height: 20px;
  126 +}
  127 +.tokenfield.input-sm .twitter-typeahead .tt-input,
  128 +.tokenfield.input-sm .twitter-typeahead .tt-hint {
  129 + height: 18px;
  130 + font-size: 12px;
  131 + line-height: 1.5;
  132 +}
  133 +.tokenfield.input-lg .twitter-typeahead .tt-input,
  134 +.tokenfield.input-lg .twitter-typeahead .tt-hint {
  135 + height: 23px;
  136 + font-size: 18px;
  137 + line-height: 1.33;
  138 +}
  139 +.tokenfield .twitter-typeahead .tt-suggestions {
  140 + font-size: 14px;
  141 +}
... ...
plugins/fb_app/views/fb_app_plugin/_load.html.slim 0 → 100644
... ... @@ -0,0 +1,39 @@
  1 += content_for :head do
  2 + = javascript_include_tag 'typeahead.bundle.js'
  3 + = stylesheet_link_tag 'typeahead'
  4 + = stylesheet_link_tag 'plugins/fb_app/stylesheets/bootstrap-tokenfield.css'
  5 + = stylesheet_link_tag 'plugins/fb_app/stylesheets/tokenfield-typeahead.css'
  6 + = javascript_include_tag 'plugins/fb_app/javascripts/bootstrap-tokenfield.js'
  7 + = javascript_include_tag 'plugins/open_graph/javascripts/open_graph.js'
  8 +
  9 +- callback = '' unless defined? callback
  10 +
  11 +#fb-root
  12 +#fb-app-modal-wrap style="display:none"
  13 + #fb-app-modal
  14 + #fb-app-modal-intro
  15 + = button_to_function 'cancel', '', "ff()", class: 'modal-button-no'
  16 + = button_to_function 'ok', '', "ff()", class: 'modal-button-yes'
  17 +
  18 +javascript:
  19 + // Adding locales:
  20 + fb_app.locales.error_empty_title = #{t('fb_app_plugin.views.myprofile.error.empty_title').to_json}
  21 + fb_app.locales.error_empty_settings = #{t('fb_app_plugin.views.myprofile.error.empty_settings').to_json}
  22 + fb_app.locales.cancel_button = #{t('fb_app_plugin.views.myprofile.catalogs.cancel_button').to_json}
  23 + fb_app.locales.confirm_removal = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_removal').to_json}
  24 + fb_app.locales.confirm_removal_button = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_removal_button').to_json}
  25 + fb_app.locales.confirm_disconnect = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_disconnect').to_json}
  26 + fb_app.locales.confirm_disconnect_button = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_disconnect_button').to_json}
  27 +
  28 + // General settings:
  29 + fb_app.current_url = #{url_for(params).to_json};
  30 + fb_app.base_url = 'https://#{environment.default_hostname}/plugin/fb_app';
  31 +
  32 + fb_app.page_tab.appId = #{FbAppPlugin.page_tab_app_credentials[:id].to_json},
  33 + fb_app.timeline.appId = #{FbAppPlugin.timeline_app_credentials[:id].to_json},
  34 + fb_app.page_tab.nextUrl = #{url_for(protocol: 'https', only_path: false).to_json}
  35 +
  36 + fb_app.fb.prepareAsyncInit(fb_app.timeline.appId, #{callback.to_json});
  37 + fb_app.fb.init();
  38 +/ must come after window.fbAsyncInit is defined
  39 += javascript_include_tag "https://connect.facebook.net/en_US/all.js"
... ...
plugins/fb_app/views/fb_app_plugin/index.html.erb 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +<div id="fb-app-wrapper">
  2 +</div>
  3 +
  4 +<%# render 'load' %>
... ...
plugins/fb_app/views/fb_app_plugin_layouts/default.html.slim 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +html
  2 + head
  3 + = noosfero_javascript
  4 + = javascript_include_tag 'fb_app'
  5 + = noosfero_stylesheets
  6 + = h stylesheet_link_tag(jquery_ui_theme_stylesheet_path)
  7 + body
  8 + = yield
... ...
plugins/fb_app/views/fb_app_plugin_myprofile/_auth.html.slim 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +- if @auth.connected?
  2 + #noosfero-identity
  3 + = profile_image profile
  4 + = profile.name
  5 + span#fb-connected.fa.fa-arrows-h
  6 + #fb-identity
  7 + = render 'identity', auth: @auth
  8 +
  9 + = button_to_function 'close', t('fb_app_plugin.views.myprofile.disconnect'), 'fb_app.timeline.disconnect()', class:'fb-app-connection-button'
  10 +
  11 +- elsif @auth.not_authorized?
  12 + = button_to_function 'login', t('fb_app_plugin.views.myprofile.connect'), 'fb_app.timeline.connect()', size: '', option: 'primary', class:'fb-app-connection-button'
  13 +- elsif @auth.expired?
  14 + = button_to_function 'login', t('fb_app_plugin.views.myprofile.reconnect'), 'fb_app.timeline.reconnect()', class:'fb-app-connection-button'
... ...
plugins/fb_app/views/fb_app_plugin_myprofile/_catalogs.html.slim 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +h2
  2 + = t'fb_app_plugin.views.myprofile.catalogs.heading'
  3 + = render 'catalogs_help' rescue nil
  4 +
  5 +- profile.fb_app_page_tabs.each do |page_tab|
  6 + = render 'fb_app_plugin_page_tab/config', page_tab: page_tab
  7 +
  8 +#new-catalog
  9 + = render 'fb_app_plugin_page_tab/config', page_tab: profile.fb_app_page_tabs.build(owner_profile: profile)
  10 +
  11 += render file: 'shared/tiny_mce', locals: {mode: 'simple'}
... ...
plugins/fb_app/views/fb_app_plugin_myprofile/_identity.html.slim 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +.fb-app-identity
  2 + - picture = auth.fb_user.picture
  3 + / fb_graph version 1 compatibility
  4 + - url = if picture.respond_to? :url then picture.url else picture end
  5 + = image_tag url
  6 + = auth.fb_user.name
  7 +
... ...
plugins/fb_app/views/fb_app_plugin_myprofile/_load.html.slim 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 += render 'fb_app_plugin/load', callback: 'fb_app.fb.checkLoginStatus()'
  2 +
  3 +javascript:
  4 + fb_app.config.url_prefix = #{url_for(action: :index).to_json}
  5 + fb_app.config.save_auth_url = #{url_for(action: :save_auth).to_json}
  6 + fb_app.config.show_login_url = #{url_for(action: :show_login).to_json}
  7 +
... ...
plugins/fb_app/views/fb_app_plugin_myprofile/_settings.html.slim 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +h1= t'fb_app_plugin.lib.plugin.name'
  2 += button :back, _('Back to control panel'), controller: 'profile_editor'
  3 +
  4 +#fb-app-connect-status
  5 + = render 'intro' rescue nil if @auth.not_authorized?
  6 + #fb-app-auth
  7 + = render 'auth'
  8 +
  9 +- if @auth.connected? or Rails.env.development?
  10 + #fb-app-catalogs
  11 + = render 'catalogs'
  12 + #fb-app-timeline
  13 + - if profile.person?
  14 + h2= t'fb_app_plugin.views.myprofile.timeline.heading'
  15 +
  16 + - unless FbAppPlugin.test_user? user
  17 + h3= t'fb_app_plugin.views.myprofile.timeline.explanation_title'
  18 + p= t'fb_app_plugin.views.myprofile.timeline.explanation_text'
  19 + - else
  20 + #track-form
  21 + = render 'track_form', context: :fb_app
  22 + - else
  23 + = t'fb_app_plugin.views.myprofile.timeline.organization_redirect',
  24 + type: t("fb_app_plugin.views.myprofile.timeline.organization_from_#{profile.class.name.underscore}"),
  25 + redirect_link: link_to(t('fb_app_plugin.views.myprofile.timeline.redirect_link'), host: FbAppPlugin.config[:app][:domain], profile: user.identifier, controller: :fb_app_plugin_myprofile)
  26 +.clean
  27 +
  28 += button :back, _('Back to control panel'), {controller: 'profile_editor'}, class: 'fb-app-final-back-button'
... ...
plugins/fb_app/views/fb_app_plugin_myprofile/index.html.slim 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 += render 'load'
  2 +javascript:
  3 + fb_app.auth.status = #{(@auth.status rescue FbAppPlugin::Auth::Status::NotAuthorized).to_json}
  4 +
  5 +#fb-app-settings
  6 + = render 'settings'
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_config.html.slim 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +.page-tab id="page-tab-#{page_tab.id || 'new'}"
  2 + - if page_tab.page_id
  3 + h3
  4 + = page_tab.title
  5 + = button_to_function_without_text 'edit', t('fb_app_plugin.views.myprofile.catalogs.edit_button') % {catalog_title: page_tab.title},
  6 + "fb_app.page_tab.config.edit($(this))", class: 'edit-tab-button'
  7 + = button_to_function_without_text 'remove', t('fb_app_plugin.views.myprofile.catalogs.remove_button') % {catalog_title: page_tab.title},
  8 + "fb_app.page_tab.config.remove($(this), '#{url_for(controller: :fb_app_plugin_page_tab, action: :destroy, id: page_tab.id)}')", class: 'edit-tab-button'
  9 + = button_without_text 'eyes', t('fb_app_plugin.views.myprofile.catalog.see_page'), page_tab.facebook_url, target: '_blank', class: 'edit-tab-button'
  10 + - else
  11 + = button_to_function 'add', t('fb_app_plugin.views.myprofile.catalogs.new'), "$(this).toggle(); $('#page-tab-new h3, #add_tab').toggle(400)"
  12 +
  13 + = render 'fb_app_plugin_page_tab/configure_form', page_tab: page_tab, signed_request: nil
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_button.html.slim 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +#manage-fb-store-ctrl
  2 + - if @page_tab.owner_profile
  3 + - if logged_in? and (user.is_admin? environment or user.is_admin? @page_tab.owner_profile)
  4 + = button :edit, t('fb_app_plugin.views.page_tab.edit_catalog'), {controller: :fb_app_plugin_myprofile, profile: @page_tab.owner_profile.identifier},
  5 + target: '_parent'
  6 + - elsif (@data[:page][:admin] rescue false)
  7 + = button :edit, t('fb_app_plugin.views.page_tab.edit_catalog'), {controller: :fb_app_plugin_page_tab, action: :admin, signed_request: @signed_requests, page_id: @page_ids}, onclick: "noosfero.modal.url(this.href); return false;"
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_form.html.slim 0 → 100644
... ... @@ -0,0 +1,37 @@
  1 +- form_id = if page_tab.id then "edit_tab_#{page_tab.id}" else "add_tab" end
  2 +
  3 +- unless page_tab.id
  4 + h3= t'fb_app_plugin.views.myprofile.catalogs.new'
  5 +
  6 += form_for page_tab, as: :page_tab, url: {controller: :fb_app_plugin_page_tab, action: :admin},
  7 + html: {id: form_id, class: "edit-page-tab", onsubmit: "return fb_app.page_tab.config.save($(this))"} do |f|
  8 +
  9 + = hidden_field_tag :signed_request, signed_request
  10 + = hidden_field_tag :page_id, page_tab.page_id
  11 + = f.hidden_field :profile_id, value: profile.id
  12 + = f.hidden_field :page_id
  13 +
  14 + = f.label :title, t("fb_app_plugin.views.myprofile.catalogs.catalog_title_label")
  15 + = f.text_field :title, class: 'form-control'
  16 +
  17 + = f.label :subtitle, t("fb_app_plugin.views.myprofile.catalogs.catalog_subtitle_label")
  18 + = f.text_area :subtitle, class: 'form-control mceEditor', id: "page-tab-subtitle-#{page_tab.id}"
  19 +
  20 + = f.label :config_type, t("fb_app_plugin.views.myprofile.catalogs.catalog_type_chooser_label")
  21 + = f.select :config_type,
  22 + page_tab.types.map{ |type| [t("fb_app_plugin.models.page_tab.types.#{if profile.enterprise? and type == :profile then :other_profile else type end}"), type] },
  23 + {}, onchange: 'fb_app.page_tab.config.change_type($(this))', class: 'form-control'
  24 +
  25 + - page_tab.types.each do |type|
  26 + div class="config-type config-type-#{type}"
  27 + = render "fb_app_plugin_page_tab/configure_#{type}", f: f, page_tab: page_tab
  28 +
  29 + - if page_tab.new_record?
  30 + = submit_button :add, t('fb_app_plugin.views.page_tab.add'), onclick: 'return fb_app.page_tab.config.add($(this.form))', class: 'fb-app-submit-page-tab-options'
  31 + - else
  32 + = submit_button :save, t('fb_app_plugin.views.page_tab.save'), class: 'fb-app-submit-page-tab-options'
  33 +
  34 +javascript:
  35 + $('document').ready(function() {
  36 + fb_app.page_tab.config.init();
  37 + });
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_own_profile.html.slim 0 → 100644
No preview for this file type
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_profile.html.slim 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 += f.label t("fb_app_plugin.views.myprofile.catalogs.profile_chooser_label")
  2 += f.text_field :profile_ids, "data-limit" => "1", "data-placeholder" => t('fb_app_plugin.views.page_tab.profile.placeholder'), onchange: 'fb_app.page_tab.config.profile.onchange($(this))'
  3 +
  4 +javascript:
  5 + $(document).ready(function() {
  6 + var selector = '#page-tab-#{page_tab.id || 'new'} .config-type-profile #page_tab_profile_ids'
  7 +
  8 + fb_app.page_tab.config.profile.onchange($(selector))
  9 +
  10 + open_graph.autocomplete.init(
  11 + #{url_for(controller: :fb_app_plugin_page_tab, action: :enterprise_search).to_json},
  12 + selector,
  13 + #{[page_tab.profile].compact.map{ |p| {value: p.id, label: render('open_graph_plugin/myprofile/ac_profile', profile: p), } }.to_json},
  14 + {tokenfield: {limit: 1}}
  15 + )
  16 +
  17 + if (#{page_tab.profile.present?.to_json})
  18 + $(selector+'-tokenfield').hide()
  19 +
  20 + $(selector)
  21 + .on('tokenfield:createdtoken', function (e) {
  22 + $(selector+'-tokenfield').hide();
  23 + })
  24 + .on('tokenfield:removedtoken', function (e) {
  25 + $(selector+'-tokenfield').show();
  26 + })
  27 + })
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_profiles.html.slim 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 += f.label t("fb_app_plugin.views.myprofile.catalogs.profiles_chooser_label")
  2 += f.text_field :profile_ids, placeholder: t('fb_app_plugin.views.page_tab.profiles.placeholder')
  3 +
  4 +javascript:
  5 + $(document).ready(function() {
  6 + open_graph.autocomplete.init(
  7 + #{url_for(controller: :fb_app_plugin_page_tab, action: :enterprise_search).to_json},
  8 + '#page-tab-#{page_tab.id || 'new'} .config-type-profiles #page_tab_profile_ids',
  9 + #{page_tab.profiles.map{ |p| {value: p.id, label: render('open_graph_plugin/myprofile/ac_profile', profile: p), } }.to_json}
  10 + )
  11 + })
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_query.html.slim 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 += f.label t("fb_app_plugin.views.myprofile.catalogs.query_label")
  2 +p= t'fb_app_plugin.views.myprofile.catalogs.query_help'
  3 += f.text_field :query, placeholder: t('fb_app_plugin.views.page_tab.query.placeholder'), class: 'form-control'
  4 +
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_footer.html.slim 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +#page-tab-footer
  2 + #page-tab-footer1.col-lg-6.col-md-6.col-sm-6.text-left
  3 + = t'fb_app_plugin.views.page_tab.footer1'
  4 + #page-tab-footer2.col-lg-6.col-md-6.col-sm-6.text-right
  5 + = t'fb_app_plugin.views.page_tab.footer2'
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_load.html.slim 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +- callback = '' unless defined? callback
  2 +
  3 += render 'fb_app_plugin/load', callback: callback
  4 +
  5 +javascript:
  6 + fb_app.page_tab.init();
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_title_and_subtitle.html.slim 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +h1= @page_tab.title
  2 +
  3 +- if @page_tab.subtitle.present?
  4 + #page-tab-subtitle= @page_tab.subtitle
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/admin.html.slim 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +#fb-app-page-tab-admin
  2 + = render 'admin_intro' rescue nil
  3 +
  4 + = render 'config', page_tab: @page_tab, page_id: @page_id, signed_request: @signed_request
  5 + = render file: 'shared/tiny_mce', locals: {mode: 'simple'}
  6 +
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/admin.js.erb 0 → 100644
... ... @@ -0,0 +1,2 @@
  1 +fb_app.page_tab.config.close(<%= @page_id.to_json %>);
  2 +
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/catalog.html.slim 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +#fb-app-catalog-wrapper class=('fb-app-standalone' if @signed_request.blank?)
  2 +
  3 + = render 'load'
  4 + = render 'title_and_subtitle'
  5 +
  6 + #product-catalog
  7 + #product-catalog-actions
  8 + - if @page_tab.config_type == :profile
  9 + - if profile and user.present? and (user.is_admin?(environment) or user.is_admin?(profile))
  10 + .product-catalog-ctrl
  11 + = button :add, _('Add product or service'), controller: :manage_products, action: :new, profile: profile.identifier
  12 + = render 'configure_button'
  13 + = content_for :product_page do
  14 + = render 'catalog/results'
  15 + = render 'catalog/search'
  16 + = render 'catalog/javascripts', external: false
  17 +
  18 + = render 'footer'
  19 +
  20 + javascript:
  21 + catalog.base_url_path = #{url_for(controller: :fb_app_plugin_page_tab, action: :index, page_id: params[:page_id], signed_request: params[:signed_request]).to_json} + '&'
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/first_load.html.slim 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +javascript:
  2 + noosfero.modal.url(#{url_for(controller: :fb_app_plugin, action: :admin, page_id: @page_ids).to_json})
  3 +
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/product.html.slim 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 += render 'title_and_subtitle'
  2 +
  3 += button :back, t('fb_app_plugin.views.page_tab.back_to_catalog'), params.except(:product_id), target: ''
  4 +
  5 += render file: "#{Rails.root}/app/views/manage_products/show.html.erb"
  6 +
  7 += button :back, t('fb_app_plugin.views.page_tab.back_to_catalog'), params.except(:product_id), target: ''
  8 +
  9 += render 'footer'
  10 +
  11 += render 'load'
... ...
plugins/oauth_client/controllers/public/oauth_client_plugin_public_controller.rb
... ... @@ -4,8 +4,8 @@ class OauthClientPluginPublicController &lt; PublicController
4 4  
5 5 def callback
6 6 auth = request.env["omniauth.auth"]
7   - user = environment.users.find_by_email(auth.info.email)
8   - user ? login(user) : signup(auth)
  7 + auth_user = environment.users.where(email: auth.info.email).first
  8 + if auth_user then login auth_user.person else signup auth end
9 9 end
10 10  
11 11 def failure
... ... @@ -20,16 +20,14 @@ class OauthClientPluginPublicController &lt; PublicController
20 20  
21 21 protected
22 22  
23   - def login(user)
  23 + def login person
24 24 provider = OauthClientPlugin::Provider.find(session[:provider_id])
25   - user_provider = user.oauth_user_providers.find_by_provider_id(provider.id)
26   - unless user_provider
27   - user_provider = user.oauth_user_providers.create(:user => user, :provider => provider, :enabled => true)
28   - end
29   - if user_provider.enabled? && provider.enabled?
30   - session[:user] = user.id
  25 + auth = person.oauth_auths.where(provider_id: provider.id).first
  26 + auth ||= person.oauth_auths.create! profile: person, provider: provider, enabled: true
  27 + if auth.enabled? && provider.enabled?
  28 + self.current_user = person.user
31 29 else
32   - session[:notice] = _("Can't login with %s") % provider.name
  30 + session[:notice] = _("Can't login with #{provider.name}")
33 31 end
34 32  
35 33 redirect_to :controller => :account, :action => :login
... ...
plugins/oauth_client/db/migrate/20150815170000_deserialize_fields_on_oauth_client_plugin_provider.rb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +class DeserializeFieldsOnOauthClientPluginProvider < ActiveRecord::Migration
  2 +
  3 + def up
  4 + add_column :oauth_client_plugin_providers, :client_id, :text
  5 + add_column :oauth_client_plugin_providers, :client_secret, :text
  6 +
  7 + OauthClientPlugin::Provider.find_each batch_size: 50 do |provider|
  8 + provider.client_id = provider.options.delete :client_id
  9 + provider.client_secret = provider.options.delete :client_secret
  10 + provider.save!
  11 + end
  12 +
  13 + add_index :oauth_client_plugin_providers, :client_id
  14 + end
  15 +
  16 + def down
  17 + say "this migration can't be reverted"
  18 + end
  19 +
  20 +end
... ...
plugins/oauth_client/db/migrate/20150815173209_add_authorization_data_to_oauth_client_user_provider.rb 0 → 100644
... ... @@ -0,0 +1,26 @@
  1 +class AddAuthorizationDataToOauthClientUserProvider < ActiveRecord::Migration
  2 +
  3 + def change
  4 + rename_table :oauth_client_plugin_user_providers, :oauth_client_plugin_auths
  5 +
  6 + add_column :oauth_client_plugin_auths, :type, :string
  7 + add_column :oauth_client_plugin_auths, :provider_user_id, :string
  8 + add_column :oauth_client_plugin_auths, :access_token, :text
  9 + add_column :oauth_client_plugin_auths, :expires_at, :datetime
  10 + add_column :oauth_client_plugin_auths, :scope, :text
  11 + add_column :oauth_client_plugin_auths, :data, :text, default: {}.to_yaml
  12 +
  13 + add_column :oauth_client_plugin_auths, :profile_id, :integer
  14 + OauthClientPlugin::Auth.find_each batch_size: 50 do |auth|
  15 + auth.profile = User.find(auth.user_id).person
  16 + auth.save!
  17 + end
  18 + remove_column :oauth_client_plugin_auths, :user_id
  19 +
  20 + add_index :oauth_client_plugin_auths, :profile_id
  21 + add_index :oauth_client_plugin_auths, :provider_id
  22 + add_index :oauth_client_plugin_auths, :provider_user_id
  23 + add_index :oauth_client_plugin_auths, :type
  24 + end
  25 +
  26 +end
... ...
plugins/oauth_client/lib/ext/profile.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +require_dependency 'profile'
  2 +
  3 +class Profile
  4 +
  5 + has_many :oauth_auths, class_name: 'OauthClientPlugin::Auth', dependent: :destroy
  6 + has_many :oauth_providers, through: :oauth_auths, source: :provider
  7 +
  8 +end
... ...
plugins/oauth_client/lib/ext/user.rb
... ... @@ -2,8 +2,14 @@ require_dependency &#39;user&#39;
2 2  
3 3 class User
4 4  
5   - has_many :oauth_user_providers, :class_name => 'OauthClientPlugin::UserProvider'
6   - has_many :oauth_providers, :through => :oauth_user_providers, :source => :provider
  5 + has_many :oauth_auths, through: :person
  6 + has_many :oauth_providers, through: :oauth_auths, source: :provider
  7 +
  8 + after_create :activate_oauth_user
  9 +
  10 + def activate_oauth_user
  11 + self.activate if oauth_providers.present?
  12 + end
7 13  
8 14 def password_required_with_oauth?
9 15 password_required_without_oauth? && oauth_providers.empty?
... ... @@ -11,17 +17,6 @@ class User
11 17  
12 18 alias_method_chain :password_required?, :oauth
13 19  
14   - after_create :activate_oauth_user
15   -
16   - def activate_oauth_user
17   - unless oauth_providers.empty?
18   - activate
19   - oauth_providers.each do |provider|
20   - OauthClientPlugin::UserProvider.create!(:user => self, :provider => provider, :enabled => true)
21   - end
22   - end
23   - end
24   -
25 20 def make_activation_code_with_oauth
26 21 oauth_providers.blank? ? make_activation_code_without_oauth : nil
27 22 end
... ...
plugins/oauth_client/lib/oauth_client_plugin.rb
... ... @@ -45,7 +45,9 @@ class OauthClientPlugin &lt; Noosfero::Plugin
45 45 true
46 46 end
47 47  
48   - OmniAuth.config.on_failure = OauthClientPluginPublicController.action(:failure)
  48 + Rails.configuration.to_prepare do
  49 + OmniAuth.config.on_failure = OauthClientPluginPublicController.action(:failure)
  50 + end
49 51  
50 52 Rails.application.config.middleware.use OmniAuth::Builder do
51 53 PROVIDERS.each do |provider, options|
... ... @@ -60,7 +62,8 @@ class OauthClientPlugin &lt; Noosfero::Plugin
60 62 provider_id = request.params['id']
61 63 provider_id ||= request.session['omniauth.params']['id'] if request.session['omniauth.params']
62 64 provider = environment.oauth_providers.find(provider_id)
63   - strategy.options.merge!(provider.options.symbolize_keys)
  65 + strategy.options.merge! client_id: provider.client_id, client_secret: provider.client_secret
  66 + strategy.options.merge! provider.options.symbolize_keys
64 67  
65 68 request.session[:provider_id] = provider_id
66 69 }
... ...
plugins/oauth_client/lib/oauth_client_plugin/provider.rb
... ... @@ -1,20 +0,0 @@
1   -class OauthClientPlugin::Provider < Noosfero::Plugin::ActiveRecord
2   -
3   - belongs_to :environment
4   -
5   - validates_presence_of :name, :strategy
6   -
7   - acts_as_having_image
8   - acts_as_having_settings :field => :options
9   -
10   - settings_items :client_id, :type => :string
11   - settings_items :client_secret, :type => :string
12   - settings_items :client_options, :type => Hash
13   -
14   - attr_accessible :name, :environment, :strategy, :client_id, :client_secret, :enabled, :client_options, :image_builder
15   -
16   - scope :enabled, :conditions => {:enabled => true}
17   -
18   - acts_as_having_image
19   -
20   -end
plugins/oauth_client/lib/oauth_client_plugin/user_provider.rb
... ... @@ -1,10 +0,0 @@
1   -class OauthClientPlugin::UserProvider < Noosfero::Plugin::ActiveRecord
2   -
3   - belongs_to :user, :class_name => 'User'
4   - belongs_to :provider, :class_name => 'OauthClientPlugin::Provider'
5   -
6   - set_table_name :oauth_client_plugin_user_providers
7   -
8   - attr_accessible :user, :provider, :enabled
9   -
10   -end
plugins/oauth_client/models/oauth_client_plugin/auth.rb 0 → 100644
... ... @@ -0,0 +1,29 @@
  1 +class OauthClientPlugin::Auth < ActiveRecord::Base
  2 +
  3 + attr_accessible :profile, :provider, :enabled,
  4 + :access_token, :expires_in
  5 +
  6 + belongs_to :profile, class_name: 'Profile'
  7 + belongs_to :provider, class_name: 'OauthClientPlugin::Provider'
  8 +
  9 + validates_presence_of :profile
  10 + validates_presence_of :provider
  11 + validates_uniqueness_of :profile_id, scope: :provider_id
  12 +
  13 + acts_as_having_settings field: :data
  14 +
  15 + def expires_in
  16 + self.expires_at - Time.now
  17 + end
  18 + def expires_in= value
  19 + self.expires_at = Time.now + value.to_i
  20 + end
  21 +
  22 + def expired?
  23 + Time.now > self.expires_at rescue true
  24 + end
  25 + def not_expired?
  26 + not self.expired?
  27 + end
  28 +
  29 +end
... ...
plugins/oauth_client/models/oauth_client_plugin/provider.rb 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +class OauthClientPlugin::Provider < ActiveRecord::Base
  2 +
  3 + belongs_to :environment
  4 +
  5 + validates_presence_of :name, :strategy
  6 +
  7 + acts_as_having_image
  8 + acts_as_having_settings field: :options
  9 +
  10 + settings_items :site, type: String
  11 + settings_items :client_options, type: Hash
  12 +
  13 + attr_accessible :name, :strategy, :enabled, :site, :image_builder,
  14 + :environment, :environment_id,
  15 + :client_id, :client_secret, :client_options
  16 +
  17 + scope :enabled, -> { where enabled: true }
  18 +
  19 + acts_as_having_image
  20 +
  21 +end
... ...
plugins/oauth_client/test/functional/oauth_client_plugin_public_controller_test.rb
1   -require File.dirname(__FILE__) + '/../test_helper'
  1 +require 'test_helper'
2 2  
3 3 class OauthClientPluginPublicControllerTest < ActionController::TestCase
4 4  
... ... @@ -21,7 +21,7 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase
21 21 end
22 22  
23 23 should 'redirect to login when user is found' do
24   - user = fast_create(User, :environment_id => environment.id)
  24 + user = create_user
25 25 auth.info.stubs(:email).returns(user.email)
26 26 auth.info.stubs(:name).returns(user.name)
27 27 session[:provider_id] = provider.id
... ... @@ -32,7 +32,7 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase
32 32 end
33 33  
34 34 should 'do not login when the provider is disabled' do
35   - user = fast_create(User, :environment_id => environment.id)
  35 + user = create_user
36 36 auth.info.stubs(:email).returns(user.email)
37 37 auth.info.stubs(:name).returns(user.name)
38 38 session[:provider_id] = provider.id
... ... @@ -44,11 +44,11 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase
44 44 end
45 45  
46 46 should 'do not login when the provider is disabled for a user' do
47   - user = fast_create(User, :environment_id => environment.id)
  47 + user = create_user
48 48 auth.info.stubs(:email).returns(user.email)
49 49 auth.info.stubs(:name).returns(user.name)
50 50 session[:provider_id] = provider.id
51   - user.oauth_user_providers.create(:user => user, :provider => provider, :enabled => false)
  51 + user.person.oauth_auths.create!(profile: user.person, provider: provider, enabled: false)
52 52  
53 53 get :callback
54 54 assert_redirected_to :controller => :account, :action => :login
... ... @@ -56,7 +56,7 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase
56 56 end
57 57  
58 58 should 'save provider when an user login with it' do
59   - user = fast_create(User, :environment_id => environment.id)
  59 + user = create_user
60 60 auth.info.stubs(:email).returns(user.email)
61 61 auth.info.stubs(:name).returns(user.name)
62 62 session[:provider_id] = provider.id
... ... @@ -66,13 +66,13 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase
66 66 end
67 67  
68 68 should 'do not duplicate relations between an user and a provider when the same provider was used again in a login' do
69   - user = fast_create(User, :environment_id => environment.id)
  69 + user = create_user
70 70 auth.info.stubs(:email).returns(user.email)
71 71 auth.info.stubs(:name).returns(user.name)
72 72 session[:provider_id] = provider.id
73 73  
74 74 get :callback
75   - assert_no_difference 'user.oauth_user_providers.count' do
  75 + assert_no_difference 'user.oauth_auths.count' do
76 76 3.times { get :callback }
77 77 end
78 78 end
... ...
plugins/oauth_client/test/unit/environment_test.rb
1   -require File.dirname(__FILE__) + '/../test_helper'
  1 +require 'test_helper'
2 2  
3 3 class UserTest < ActiveSupport::TestCase
4 4  
... ...
plugins/oauth_client/test/unit/oauth_client_plugin_test.rb
1   -require File.dirname(__FILE__) + '/../test_helper'
  1 +require 'test_helper'
2 2  
3 3 class OauthClientPluginTest < ActiveSupport::TestCase
4 4  
... ...
plugins/oauth_client/test/unit/user_test.rb
1   -require File.dirname(__FILE__) + '/../test_helper'
  1 +require 'test_helper'
2 2  
3 3 class UserTest < ActiveSupport::TestCase
4 4  
... ...
plugins/open_graph/db/migrate/20150814200324_add_story_and_published_at_to_open_graph_plugin_activity.rb 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +class AddStoryAndPublishedAtToOpenGraphPluginActivity < ActiveRecord::Migration
  2 +
  3 + def change
  4 + add_column :open_graph_plugin_tracks, :published_at, :datetime
  5 + add_column :open_graph_plugin_tracks, :story, :string
  6 + add_index :open_graph_plugin_tracks, :published_at
  7 + add_index :open_graph_plugin_tracks, :story
  8 + end
  9 +
  10 +end
... ...
plugins/open_graph/lib/open_graph_plugin.rb
... ... @@ -17,5 +17,9 @@ module OpenGraphPlugin
17 17 Thread.current[:open_graph_context] = value
18 18 end
19 19  
  20 + def self.debug? actor=nil
  21 + !Rails.env.production?
  22 + end
  23 +
20 24 end
21 25  
... ...
plugins/open_graph/lib/open_graph_plugin/publisher.rb
1 1  
2 2 class OpenGraphPlugin::Publisher
3 3  
4   - attr_accessor :actions
5   - attr_accessor :objects
6   -
7 4 def self.default
8 5 @default ||= self.new
9 6 end
10 7  
11 8 def initialize attributes = {}
12   - # defaults
13   - self.actions = OpenGraphPlugin::Stories::DefaultActions
14   - self.objects = OpenGraphPlugin::Stories::DefaultObjects
15   -
16 9 attributes.each do |attr, value|
17 10 self.send "#{attr}=", value
18 11 end
19 12 end
20 13  
21   - def publish actor, story_defs, object_data_url
22   - raise 'abstract method called'
23   - end
24   -
25 14 def publish_stories object_data, actor, stories
26 15 stories.each do |story|
27 16 begin
28 17 self.publish_story object_data, actor, story
29 18 rescue => e
  19 + raise unless Rails.env.production?
30 20 ExceptionNotifier.notify_exception e
31 21 end
32 22 end
33 23 end
34 24  
35   - def update_delay
36   - 1.day
37   - end
38   -
39   - # only publish recent objects to avoid multiple publications
40   - def recent_publish? actor, object_type, object_data_url
41   - activity_params = {actor_id: actor.id, object_type: object_type, object_data_url: object_data_url}
42   - activity = OpenGraphPlugin::Activity.where(activity_params).first
43   - activity.present? and activity.created_at <= self.update_delay.from_now
44   - end
45   -
46 25 def publish_story object_data, actor, story
47   - OpenGraphPlugin.context = self.context
48   - defs = OpenGraphPlugin::Stories::Definitions[story]
49   - passive = defs[:passive]
50   -
51   - print_debug "open_graph: publish_story #{story}" if debug? actor
52   - match_criteria = if (ret = self.call defs[:criteria], object_data, actor).nil? then true else ret end
53   - return unless match_criteria
54   - print_debug "open_graph: #{story} match criteria" if debug? actor
55   - match_condition = if (ret = self.call defs[:publish_if], object_data, actor).nil? then true else ret end
56   - return unless match_condition
57   - print_debug "open_graph: #{story} match publish_if" if debug? actor
58   -
59   - actors = self.story_trackers defs, actor, object_data
60   - return if actors.blank?
61   - print_debug "open_graph: #{story} has enabled trackers" if debug? actor
62   -
63   - if publish = defs[:publish]
64   - begin
65   - instance_exec actor, object_data, &publish
66   - rescue => e
67   - print_debug "open_graph: can't publish story: #{e.message}" if debug? actor
68   - ExceptionNotifier.notify_exception e
69   - end
70   - else
71   - # force profile identifier for custom domains and fixed host. see og_url_for
72   - object_profile = self.call(story_defs[:object_profile], object_data) || object_data.profile rescue nil
73   - extra_params = if object_profile then {profile: object_profile.identifier} else {} end
74   -
75   - custom_object_data_url = self.call defs[:object_data_url], object_data, actor
76   - object_data_url = if passive then self.passive_url_for object_data, custom_object_data_url, defs, extra_params else self.url_for object_data, custom_object_data_url, extra_params end
77   -
78   - actors.each do |actor|
79   - print_debug "open_graph: start publishing" if debug? actor
80   - begin
81   - self.publish actor, defs, object_data_url
82   - rescue => e
83   - print_debug "open_graph: can't publish story: #{e.message}" if debug? actor
84   - ExceptionNotifier.notify_exception e
85   - end
86   - end
87   - end
88   - end
89   -
90   - def story_trackers story_defs, actor, object_data
91   - passive = story_defs[:passive]
92   - trackers = []
93   -
94   - track_configs = Array(story_defs[:track_config]).compact.map(&:constantize)
95   - return if track_configs.empty?
96   - print_debug "open_graph: using configs: #{track_configs.map(&:name).inspect}" if debug? actor
97   -
98   - if passive
99   - object_profile = self.call(story_defs[:object_profile], object_data) || object_data.profile rescue nil
100   - return unless object_profile
101   -
102   - track_configs.each do |c|
103   - trackers.concat c.trackers_to_profile(object_profile)
104   - end.flatten
105   -
106   - trackers.select! do |t|
107   - track_configs.any?{ |c| c.enabled? self.context, t }
108   - end
109   - else #active
110   - object_actor = self.call(story_defs[:object_actor], object_data) || object_data.profile rescue nil
111   - return unless object_actor and object_actor.person?
112   - custom_actor = self.call(story_defs[:custom_actor], object_data)
113   - actor = custom_actor if custom_actor
114   -
115   - match_track = track_configs.any? do |c|
116   - c.enabled?(self.context, actor) and
117   - actor.send("open_graph_#{c.track_name}_track_configs").where(object_type: story_defs[:object_type]).first
118   - end
119   - trackers << actor if match_track
120   - end
121   -
122   - trackers
  26 + OpenGraphPlugin.context = OpenGraphPlugin::Activity.context
  27 + a = OpenGraphPlugin::Activity.new object_data: object_data, actor: actor, story: story
  28 + a.dispatch_publications
  29 + a.save
123 30 end
124 31  
125 32 protected
126 33  
127   - include MetadataPlugin::UrlHelper
128   -
129   - def register_publish attributes
130   - OpenGraphPlugin::Activity.create! attributes
131   - end
132   -
133   - # Call don't ask: move to a og_url method inside object
134   - def url_for object, custom_url=nil, extra_params={}
135   - return custom_url if custom_url.is_a? String
136   - url = custom_url || if object.is_a? Profile then og_profile_url object else object.url end
137   - # for profile when custom domain is used
138   - url.merge! profile: object.profile.identifier if object.respond_to? :profile
139   - url.merge! extra_params
140   - self.og_url_for url
141   - end
142   -
143   - def passive_url_for object, custom_url, story_defs, extra_params={}
144   - object_type = story_defs[:object_type]
145   - extra_params.merge! og_type: MetadataPlugin.og_types[object_type]
146   - self.url_for object, custom_url, extra_params
147   - end
148   -
149   - def call p, *args
150   - p and instance_exec *args, &p
151   - end
152   -
153   - def context
154   - :open_graph
155   - end
156   -
157   - def print_debug msg
158   - puts msg
159   - Delayed::Worker.logger.debug msg
160   - end
161   - def debug? actor=nil
162   - !Rails.env.production?
163   - end
164   -
165 34 end
166 35  
... ...
plugins/open_graph/lib/open_graph_plugin/stories.rb
... ... @@ -99,13 +99,6 @@ class OpenGraphPlugin::Stories
99 99 publish_if: proc do |article, actor|
100 100 article.published?
101 101 end,
102   - object_data_url: proc do |article, actor|
103   - url = article.url
104   - if og_type = MetadataPlugin::og_types[:forum]
105   - url[:og_type] = og_type
106   - end
107   - url
108   - end,
109 102 },
110 103  
111 104 # these a published as passive to give focus to the enterprise
... ...
plugins/open_graph/lib/open_graph_plugin/url_helper.rb 0 → 100644
... ... @@ -0,0 +1,24 @@
  1 +module OpenGraphPlugin::UrlHelper
  2 +
  3 + protected
  4 +
  5 + include MetadataPlugin::UrlHelper
  6 +
  7 + # Call don't ask: move to a og_url method inside object
  8 + def url_for object, custom_url=nil, extra_params={}
  9 + return custom_url if custom_url.is_a? String
  10 + url = custom_url || if object.is_a? Profile then og_profile_url object else object.url end
  11 + # for profile when custom domain is used
  12 + url.merge! profile: object.profile.identifier if object.respond_to? :profile
  13 + url.merge! extra_params
  14 + self.og_url_for url
  15 + end
  16 +
  17 + def passive_url_for object, custom_url, story_defs, extra_params={}
  18 + object_type = story_defs[:object_type]
  19 + og_type = MetadataPlugin.og_types[object_type]
  20 + extra_params.merge! og_type: og_type if og_type.present?
  21 + self.url_for object, custom_url, extra_params
  22 + end
  23 +
  24 +end
... ...
plugins/open_graph/models/open_graph_plugin/activity.rb
1 1 # This is a log of activities, unlike ActivityTrack that is a configuration
2 2 class OpenGraphPlugin::Activity < OpenGraphPlugin::Track
3 3  
  4 + Defs = OpenGraphPlugin::Stories::Definitions
  5 +
  6 + UpdateDelay = 1.day
  7 +
  8 + class_attribute :actions, :objects
  9 + self.actions = OpenGraphPlugin::Stories::DefaultActions
  10 + self.objects = OpenGraphPlugin::Stories::DefaultObjects
  11 +
  12 + validates_presence_of :action
  13 + validates_presence_of :object_type
  14 +
4 15 # subclass this to define (e.g. FbAppPlugin::Activity)
5 16 def scrape
  17 + raise NotImplementedError
  18 + end
  19 + def publish! actor = self.actor
  20 + self.published_at = Time.now
  21 + print_debug "open_graph: published with success" if debug? actor
  22 + end
  23 +
  24 + def defs
  25 + @defs ||= Defs[self.story.to_sym]
  26 + end
  27 + def object_profile
  28 + @object_profile ||= self.call(self.defs[:object_profile], self.object_data) || self.object_data.profile rescue nil
  29 + end
  30 + def track_configs
  31 + @track_configs ||= Array(self.defs[:track_config]).compact.map(&:constantize)
  32 + end
  33 + def match_criteria?
  34 + if (ret = self.call self.defs[:criteria], self.object_data, self.actor).nil? then true else ret end
  35 + end
  36 + def match_publish_if?
  37 + if (ret = self.call self.defs[:publish_if], self.object_data, self.actor).nil? then true else ret end
  38 + end
  39 + def custom_object_data_url
  40 + @custom_object_data_url ||= self.call defs[:object_data_url], self.object_data, self.actor
  41 + end
  42 + def object_actor
  43 + @object_actor ||= self.call(self.defs[:object_actor], self.object_data) || self.object_data.profile rescue nil
  44 + end
  45 + def custom_actor
  46 + @custom_actor ||= self.call self.defs[:custom_actor], self.object_data
  47 + end
  48 +
  49 + def set_object_data_url
  50 + # force profile identifier for custom domains and fixed host. see og_url_for
  51 + extra_params = if self.object_profile then {profile: self.object_profile.identifier} else {} end
  52 +
  53 + self.object_data_url = if self.defs[:passive] then self.passive_url_for self.object_data, self.custom_object_data_url, self.defs, extra_params else self.url_for self.object_data, self.custom_object_data_url, extra_params end
  54 + end
  55 +
  56 + def dispatch_publications
  57 + print_debug "open_graph: dispatch_publications of #{story}" if debug? self.actor
  58 +
  59 + return unless self.match_criteria?
  60 + print_debug "open_graph: #{story} match criteria" if debug? self.actor
  61 + return unless self.match_publish_if?
  62 + print_debug "open_graph: #{story} match publish_if" if debug? self.actor
  63 + return unless (actors = self.trackers).present?
  64 + print_debug "open_graph: #{story} has enabled trackers" if debug? self.actor
  65 +
  66 + self.set_object_data_url
  67 + self.action = self.class.actions[self.defs[:action]]
  68 + self.object_type = self.class.objects[self.defs[:object_type]]
  69 +
  70 + print_debug "open_graph: start publishing" if debug? actor
  71 + unless (publish = self.defs[:publish]).present?
  72 + actors.each do |actor|
  73 + begin
  74 + self.publish! actor
  75 + rescue => e
  76 + print_debug "open_graph: can't publish story: #{e.message}" if debug? actor
  77 + raise unless Rails.env.production?
  78 + ExceptionNotifier.notify_exception e
  79 + end
  80 + end
  81 + else # custom publish proc
  82 + begin
  83 + instance_exec self.actor, self.object_data, &publish
  84 + rescue => e
  85 + print_debug "open_graph: can't publish story: #{e.message}" if debug? self.actor
  86 + raise unless Rails.env.production?
  87 + ExceptionNotifier.notify_exception e
  88 + end
  89 + end
  90 + end
  91 +
  92 + def trackers
  93 + @trackers ||= begin
  94 + return if self.track_configs.empty?
  95 + trackers = []
  96 +
  97 + print_debug "open_graph: using configs: #{self.track_configs.map(&:name).inspect}" if debug? self.actor
  98 +
  99 + if self.defs[:passive]
  100 + return unless self.object_profile
  101 +
  102 + self.track_configs.each do |c|
  103 + trackers.concat c.trackers_to_profile(self.object_profile)
  104 + end.flatten
  105 +
  106 + trackers.select! do |t|
  107 + self.track_configs.any?{ |c| c.enabled? self.context, t }
  108 + end
  109 + else #active
  110 + return unless self.object_actor and self.object_actor.person?
  111 + actor = self.custom_actor || self.actor
  112 +
  113 + match_track = self.track_configs.any? do |c|
  114 + c.enabled?(self.context, actor) and
  115 + actor.send("open_graph_#{c.track_name}_track_configs").where(object_type: self.defs[:object_type]).first
  116 + end
  117 + trackers << actor if match_track
  118 + end
  119 +
  120 + trackers
  121 + end
  122 + end
  123 +
  124 + protected
  125 +
  126 + include OpenGraphPlugin::UrlHelper
  127 +
  128 + def update_delay
  129 + UpdateDelay
  130 + end
  131 +
  132 + # only publish recent objects to avoid multiple publications
  133 + def recent_publish? actor, object_type, object_data_url
  134 + activity_params = {actor_id: actor.id, object_type: object_type, object_data_url: object_data_url}
  135 + activity = OpenGraphPlugin::Activity.where(activity_params).first
  136 + activity.present? and activity.created_at <= self.update_delay.from_now
  137 + end
  138 +
  139 + def call p, *args
  140 + p and instance_exec *args, &p
6 141 end
7 142  
8 143 end
... ...
plugins/open_graph/models/open_graph_plugin/track.rb
1 1 class OpenGraphPlugin::Track < ActiveRecord::Base
2 2  
  3 + class_attribute :context
  4 + self.context = :open_graph
  5 +
3 6 attr_accessible :type, :context, :tracker_id, :tracker, :actor_id, :action,
4   - :object_type, :object_data, :object_data_id, :object_data_type, :object_data_url
  7 + :object_type, :object_data_id, :object_data_type, :object_data_url,
  8 + :story, :object_data, :actor
5 9  
6 10 belongs_to :tracker, class_name: 'Profile'
7 11 belongs_to :actor, class_name: 'Profile'
8 12 belongs_to :object_data, polymorphic: true
9 13  
10   - validates_presence_of :context
11 14 before_validation :set_context
12 15  
13 16 def self.objects
... ... @@ -21,7 +24,15 @@ class OpenGraphPlugin::Track &lt; ActiveRecord::Base
21 24 protected
22 25  
23 26 def set_context
24   - self.context = OpenGraphPlugin.context
  27 + self[:context] = self.class.context
  28 + end
  29 +
  30 + def print_debug msg
  31 + puts msg
  32 + Delayed::Worker.logger.debug msg
  33 + end
  34 + def debug? actor=nil
  35 + OpenGraphPlugin.debug? actor
25 36 end
26 37  
27 38 end
... ...
plugins/open_graph/plugin.yml
... ... @@ -1,3 +0,0 @@
1   -name: open_graph
2   -dependencies:
3   - - metadata
plugins/open_graph/test/unit/open_graph_graph/publisher_test.rb
... ... @@ -2,14 +2,16 @@ require &quot;test_helper&quot;
2 2  
3 3 class OpenGraphPlugin::PublisherTest < ActiveSupport::TestCase
4 4  
  5 + include OpenGraphPlugin::UrlHelper
  6 +
5 7 def setup
6 8 @actor = create_user.person
7 9 User.current = @actor.user
8   - @stories = OpenGraphPlugin::Stories::Definitions
9 10 @publisher = OpenGraphPlugin::Publisher.new
10 11 OpenGraphPlugin::Stories.stubs(:publishers).returns([@publisher])
11   - @publisher.stubs(:context).returns(:open_graph)
12   - @publisher.stubs(:og_domain).returns('noosfero.net')
  12 + # for MetadataPlugin::UrlHelper#og_url_for
  13 + stubs(:og_domain).returns('noosfero.net')
  14 + OpenGraphPlugin::Activity.any_instance.stubs(:og_domain).returns('noosfero.net')
13 15 end
14 16  
15 17 should "publish only tracked stuff" do
... ... @@ -46,66 +48,70 @@ class OpenGraphPlugin::PublisherTest &lt; ActiveSupport::TestCase
46 48  
47 49 # active
48 50 User.current = @actor.user
  51 + user = User.current.person
  52 +
  53 + blog = Blog.create! profile: user, name: 'blog'
  54 + blog_post = TinyMceArticle.create! profile: user, parent: blog, name: 'blah', author: user
  55 + assert_last_activity user, :create_an_article, url_for(blog_post)
  56 +
  57 + gallery = Gallery.create! name: 'gallery', profile: user
  58 + image = UploadedFile.create! uploaded_data: fixture_file_upload('/files/rails.png', 'image/png'), parent: gallery, profile: user
  59 + assert_last_activity user, :add_an_image, url_for(image, image.url.merge(view: true))
  60 +
  61 + document = UploadedFile.create! uploaded_data: fixture_file_upload('/files/doctest.en.xhtml', 'text/html'), profile: user
  62 + assert_last_activity user, :add_a_document, url_for(document, document.url.merge(view: true))
  63 +
  64 + event = Event.create! name: 'event', profile: user
  65 + assert_last_activity user, :create_an_event, url_for(event)
  66 +
  67 + forum = Forum.create! name: 'forum', profile: user
  68 + topic = TinyMceArticle.create! profile: user, parent: forum, name: 'blah2', author: user
  69 + assert_last_activity user, :start_a_discussion, url_for(topic, topic.url.merge(og_type: MetadataPlugin.og_types[:forum]))
49 70  
50   - blog = Blog.create! profile: @actor, name: 'blog'
51   - blog_post = TinyMceArticle.new profile: User.current.person, parent: blog, name: 'blah', author: User.current.person
52   - @publisher.expects(:publish).with(User.current.person, @stories[:create_an_article], @publisher.send(:url_for, blog_post))
53   - blog_post.save!
54   -
55   - gallery = Gallery.create! name: 'gallery', profile: User.current.person
56   - image = UploadedFile.new uploaded_data: fixture_file_upload('/files/rails.png', 'image/png'), parent: gallery, profile: User.current.person
57   - @publisher.expects(:publish).with(User.current.person, @stories[:add_an_image], @publisher.send(:url_for, image, image.url.merge(view: true)))
58   - image.save!
59   -
60   - document = UploadedFile.new uploaded_data: fixture_file_upload('/files/doctest.en.xhtml', 'text/html'), profile: User.current.person
61   - @publisher.expects(:publish).with(User.current.person, @stories[:add_a_document], @publisher.send(:url_for, document, document.url.merge(view: true)))
62   - document.save!
63   -
64   - event = Event.new name: 'event', profile: User.current.person
65   - @publisher.expects(:publish).with(User.current.person, @stories[:create_an_event], @publisher.send(:url_for, event))
66   - event.save!
67   -
68   - forum = Forum.create! name: 'forum', profile: User.current.person
69   - topic = TinyMceArticle.new profile: User.current.person, parent: forum, name: 'blah2', author: User.current.person
70   - @publisher.expects(:publish).with(User.current.person, @stories[:start_a_discussion], @publisher.send(:url_for, topic, topic.url.merge(og_type: MetadataPlugin.og_types[:forum])))
71   - topic.save!
72   -
73   - @publisher.expects(:publish).with(@actor, @stories[:make_friendship_with], @publisher.send(:url_for, @other_actor)).twice
74   - @publisher.expects(:publish).with(@other_actor, @stories[:make_friendship_with], @publisher.send(:url_for, @actor)).twice
75   - AddFriend.create!(person: @actor, friend: @other_actor).finish
76   - Friendship.remove_friendship @actor, @other_actor
  71 + AddFriend.create!(person: user, friend: @other_actor).finish
  72 + #assert_last_activity user, :make_friendship_with, url_for(@other_actor)
  73 + Friendship.remove_friendship user, @other_actor
77 74 # friend verb is groupable
78   - AddFriend.create!(person: @actor, friend: @other_actor).finish
  75 + AddFriend.create!(person: user, friend: @other_actor).finish
  76 + #assert_last_activity @other_actor, :make_friendship_with, url_for(user)
79 77  
80   - @publisher.expects(:publish).with(User.current.person, @stories[:favorite_a_sse_initiative], @publisher.send(:url_for, @enterprise))
81   - @enterprise.fans << User.current.person
  78 + @enterprise.fans << user
  79 + assert_last_activity user, :favorite_a_sse_initiative, url_for(@enterprise)
82 80  
83 81 # active but published as passive
84 82 User.current = @actor.user
  83 + user = User.current.person
85 84  
86   - blog_post = TinyMceArticle.new profile: @enterprise, parent: @enterprise.blog, name: 'blah', author: User.current.person
87   - story = @stories[:announce_news_from_a_sse_initiative]
88   - @publisher.expects(:publish).with(User.current.person, story, @publisher.send(:passive_url_for, blog_post, nil, story))
89   - blog_post.save!
  85 + blog_post = TinyMceArticle.create! profile: @enterprise, parent: @enterprise.blog, name: 'blah', author: user
  86 + story = :announce_news_from_a_sse_initiative
  87 + assert_last_activity user, story, passive_url_for(blog_post, nil, OpenGraphPlugin::Stories::Definitions[story])
90 88  
91 89 # passive
92 90 User.current = @other_actor.user
  91 + user = User.current.person
93 92  
94 93 # fan
95   - blog_post = TinyMceArticle.new profile: @enterprise, parent: @enterprise.blog, name: 'blah2', author: User.current.person
96   - story = @stories[:announce_news_from_a_sse_initiative]
97   - @publisher.expects(:publish).with(@actor, story, 'http://noosfero.net/coop/blog/blah2')
98   - blog_post.save!
  94 + blog_post = TinyMceArticle.create! profile: @enterprise, parent: @enterprise.blog, name: 'blah2', author: user
  95 + assert_last_activity user, :announce_news_from_a_sse_initiative, 'http://noosfero.net/coop/blog/blah2'
99 96 # member
100   - blog_post = TinyMceArticle.new profile: @myenterprise, parent: @myenterprise.blog, name: 'blah2', author: User.current.person
101   - story = @stories[:announce_news_from_a_sse_initiative]
102   - @publisher.expects(:publish).with(@actor, story, 'http://noosfero.net/mycoop/blog/blah2')
103   - blog_post.save!
104   -
105   - blog_post = TinyMceArticle.new profile: @community, parent: @community.blog, name: 'blah', author: User.current.person
106   - story = @stories[:announce_news_from_a_community]
107   - @publisher.expects(:publish).with(@actor, story, 'http://noosfero.net/comm/blog/blah')
108   - blog_post.save!
  97 + blog_post = TinyMceArticle.create! profile: @myenterprise, parent: @myenterprise.blog, name: 'blah2', author: user
  98 + assert_last_activity user, :announce_news_from_a_sse_initiative, 'http://noosfero.net/mycoop/blog/blah2'
  99 +
  100 + blog_post = TinyMceArticle.create! profile: @community, parent: @community.blog, name: 'blah', author: user
  101 + assert_last_activity user, :announce_news_from_a_community, 'http://noosfero.net/comm/blog/blah'
  102 + end
  103 +
  104 + protected
  105 +
  106 + def assert_activity activity, actor, story, object_data_url
  107 + assert_equal actor, activity.actor, actor
  108 + assert_equal story.to_s, activity.story
  109 + assert_equal object_data_url, activity.object_data_url
  110 + end
  111 +
  112 + def assert_last_activity actor, story, object_data_url
  113 + a = OpenGraphPlugin::Activity.order('id DESC').first
  114 + assert_activity a, actor, story, object_data_url
109 115 end
110 116  
111 117 end
... ...