Commit 1e0cc2a4e01f13aa5dc8f3d96a00591cee7f681b
Exists in
fix_gallery_image_url
and in
3 other branches
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
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | + | ... | ... |
... | ... | @@ -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 | + | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | + | ... | ... |
... | ... | @@ -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 | + | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | + | ... | ... |
... | ... | @@ -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
722 Bytes
3.31 KB
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">×</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 | +})); | ... | ... |
... | ... | @@ -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 | + | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
... | ... | @@ -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_layouts/default.html.slim
0 → 100644
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
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
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
plugins/fb_app/views/fb_app_plugin_page_tab/_footer.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/_load.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/_title_and_subtitle.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/admin.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/admin.js.erb
0 → 100644
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
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 < 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 < 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/user.rb
... | ... | @@ -2,8 +2,14 @@ require_dependency 'user' |
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 < 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 < 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 |
... | ... | @@ -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 < 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 < 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 < 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 < 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 < 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
plugins/oauth_client/test/unit/oauth_client_plugin_test.rb
plugins/oauth_client/test/unit/user_test.rb
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
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 | ... | ... |
... | ... | @@ -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 < 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
plugins/open_graph/test/unit/open_graph_graph/publisher_test.rb
... | ... | @@ -2,14 +2,16 @@ require "test_helper" |
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 < 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 | ... | ... |