Commit c686ddb28d936095255c5792fcac75f271c43068
Exists in
master
and in
20 other branches
Merge branch 'noosfero' into rails4
Showing
183 changed files
with
5419 additions
and
573 deletions
Show diff stats
Too many changes.
To preserve performance only 100 of 183 files displayed.
app/controllers/public/account_controller.rb
... | ... | @@ -46,8 +46,12 @@ class AccountController < ApplicationController |
46 | 46 | |
47 | 47 | self.current_user = plugins_alternative_authentication |
48 | 48 | |
49 | - self.current_user ||= User.authenticate(params[:user][:login], params[:user][:password], environment) if params[:user] | |
50 | - | |
49 | + begin | |
50 | + self.current_user ||= User.authenticate(params[:user][:login], params[:user][:password], environment) if params[:user] | |
51 | + rescue User::UserNotActivated => e | |
52 | + session[:notice] = e.message | |
53 | + return | |
54 | + end | |
51 | 55 | if logged_in? |
52 | 56 | check_join_in_community(self.current_user) |
53 | 57 | ... | ... |
app/controllers/public/search_controller.rb
... | ... | @@ -92,10 +92,10 @@ class SearchController < PublicController |
92 | 92 | |
93 | 93 | def events |
94 | 94 | if params[:year].blank? && params[:year].blank? && params[:day].blank? |
95 | - @date = Date.today | |
95 | + @date = DateTime.now | |
96 | 96 | else |
97 | - year = (params[:year] ? params[:year].to_i : Date.today.year) | |
98 | - month = (params[:month] ? params[:month].to_i : Date.today.month) | |
97 | + year = (params[:year] ? params[:year].to_i : DateTime.now.year) | |
98 | + month = (params[:month] ? params[:month].to_i : DateTime.now.month) | |
99 | 99 | day = (params[:day] ? params[:day].to_i : 1) |
100 | 100 | @date = build_date(year, month, day) |
101 | 101 | end |
... | ... | @@ -106,9 +106,7 @@ class SearchController < PublicController |
106 | 106 | @events = @category ? |
107 | 107 | environment.events.by_day(@date).in_category(Category.find(@category_id)).paginate(:per_page => per_page, :page => params[:page]) : |
108 | 108 | environment.events.by_day(@date).paginate(:per_page => per_page, :page => params[:page]) |
109 | - end | |
110 | - | |
111 | - if params[:year] || params[:month] | |
109 | + elsif params[:year] || params[:month] | |
112 | 110 | @events = @category ? |
113 | 111 | environment.events.by_month(@date).in_category(Category.find(@category_id)).paginate(:per_page => per_page, :page => params[:page]) : |
114 | 112 | environment.events.by_month(@date).paginate(:per_page => per_page, :page => params[:page]) | ... | ... |
app/helpers/application_helper.rb
app/helpers/blog_helper.rb
... | ... | @@ -6,7 +6,13 @@ module BlogHelper |
6 | 6 | @article = article |
7 | 7 | hidden_field_tag('article[published]', 1) + |
8 | 8 | hidden_field_tag('article[accept_comments]', 0) + |
9 | - visibility_options(article,tokenized_children) | |
9 | + visibility_options(article,tokenized_children) + | |
10 | + content_tag('h4', _('Visualization of posts')) + | |
11 | + content_tag( | |
12 | + 'div', | |
13 | + check_box(:article, :display_preview) + | |
14 | + content_tag('label', _('I want to display the preview of posts before the text'), :for => 'article_display_preview') | |
15 | + ) | |
10 | 16 | end |
11 | 17 | |
12 | 18 | def cms_label_for_new_children | ... | ... |
app/helpers/content_viewer_helper.rb
... | ... | @@ -51,7 +51,7 @@ module ContentViewerHelper |
51 | 51 | elsif date_format == 'past_time' |
52 | 52 | left_time = true |
53 | 53 | end |
54 | - content_tag('span', show_date(article.published_at, use_numbers , year, left_time), :class => 'date') | |
54 | + content_tag('span', show_time(article.published_at, use_numbers , year, left_time), :class => 'date') | |
55 | 55 | end |
56 | 56 | |
57 | 57 | def link_to_comments(article, args = {}) | ... | ... |
app/helpers/dates_helper.rb
... | ... | @@ -43,9 +43,14 @@ module DatesHelper |
43 | 43 | end |
44 | 44 | |
45 | 45 | # formats a datetime for displaying. |
46 | - def show_time(time) | |
47 | - if time | |
48 | - _('%{day} %{month} %{year}, %{hour}:%{minutes}') % { :year => time.year, :month => month_name(time.month), :day => time.day, :hour => time.hour, :minutes => time.strftime("%M") } | |
46 | + def show_time(time, use_numbers = false, year = true, left_time = false) | |
47 | + if time && use_numbers | |
48 | + _('%{month}/%{day}/%{year}, %{hour}:%{minutes}') % { :year => (year ? time.year : ''), :month => time.month, :day => time.day, :hour => time.hour, :minutes => time.strftime("%M") } | |
49 | + elsif time && left_time | |
50 | + date_format = time_ago_in_words(time) | |
51 | + elsif time | |
52 | + date_format = year ? _('%{month_name} %{day}, %{year} %{hour}:%{minutes}') : _('%{month_name} %{day} %{hour}:%{minutes}') | |
53 | + date_format % { :day => time.day, :month_name => month_name(time.month), :year => time.year, :hour => time.hour, :minutes => time.strftime("%M") } | |
49 | 54 | else |
50 | 55 | '' |
51 | 56 | end |
... | ... | @@ -53,7 +58,7 @@ module DatesHelper |
53 | 58 | |
54 | 59 | def show_period(date1, date2 = nil, use_numbers = false) |
55 | 60 | if (date1 == date2) || (date2.nil?) |
56 | - show_date(date1, use_numbers) | |
61 | + show_time(date1, use_numbers) | |
57 | 62 | else |
58 | 63 | if date1.year == date2.year |
59 | 64 | if date1.month == date2.month |
... | ... | @@ -72,8 +77,8 @@ module DatesHelper |
72 | 77 | end |
73 | 78 | else |
74 | 79 | _('from %{date1} to %{date2}') % { |
75 | - :date1 => show_date(date1, use_numbers), | |
76 | - :date2 => show_date(date2, use_numbers) | |
80 | + :date1 => show_time(date1, use_numbers), | |
81 | + :date2 => show_time(date2, use_numbers) | |
77 | 82 | } |
78 | 83 | end |
79 | 84 | end |
... | ... | @@ -106,18 +111,18 @@ module DatesHelper |
106 | 111 | |
107 | 112 | def build_date(year, month, day = 1) |
108 | 113 | if year.blank? and month.blank? and day.blank? |
109 | - Date.today | |
114 | + DateTime.now | |
110 | 115 | else |
111 | 116 | if year.blank? |
112 | - year = Date.today.year | |
117 | + year = DateTime.now.year | |
113 | 118 | end |
114 | 119 | if month.blank? |
115 | - month = Date.today.month | |
120 | + month = DateTime.now.month | |
116 | 121 | end |
117 | 122 | if day.blank? |
118 | 123 | day = 1 |
119 | 124 | end |
120 | - Date.new(year.to_i, month.to_i, day.to_i) | |
125 | + DateTime.new(year.to_i, month.to_i, day.to_i) | |
121 | 126 | end |
122 | 127 | end |
123 | 128 | ... | ... |
app/helpers/events_helper.rb
... | ... | @@ -16,7 +16,7 @@ module EventsHelper |
16 | 16 | |
17 | 17 | content_tag( 'tr', |
18 | 18 | content_tag('td', |
19 | - content_tag('div', show_date(article.start_date) + ( article.end_date.nil? ? '' : (_(" to ") + show_date(article.end_date))),:class => 'event-date' ) + | |
19 | + content_tag('div', show_time(article.start_date) + ( article.end_date.nil? ? '' : (_(" to ") + show_time(article.end_date))),:class => 'event-date' ) + | |
20 | 20 | content_tag('div',link_to(article.name,article.url),:class => 'event-title') + |
21 | 21 | content_tag('div',(article.address.nil? or article.address == '') ? '' : (_('Place: ') + article.address),:class => 'event-place') |
22 | 22 | ) |
... | ... | @@ -30,7 +30,7 @@ module EventsHelper |
30 | 30 | # the day itself |
31 | 31 | date, |
32 | 32 | # is there any events in this date? |
33 | - events.any? {|event| event.date_range.include?(date)}, | |
33 | + events.any? {|event| event.date_range.cover?(date)}, | |
34 | 34 | # is this date in the current month? |
35 | 35 | true |
36 | 36 | ] | ... | ... |
app/helpers/forms_helper.rb
... | ... | @@ -151,7 +151,7 @@ module FormsHelper |
151 | 151 | datepicker_options[:close_text] ||= _('Done') |
152 | 152 | datepicker_options[:constrain_input] ||= true |
153 | 153 | datepicker_options[:current_text] ||= _('Today') |
154 | - datepicker_options[:date_format] ||= 'mm/dd/yy' | |
154 | + datepicker_options[:date_format] ||= 'yy/mm/dd' | |
155 | 155 | datepicker_options[:day_names] ||= [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')] |
156 | 156 | datepicker_options[:day_names_min] ||= [_('Su'), _('Mo'), _('Tu'), _('We'), _('Th'), _('Fr'), _('Sa')] |
157 | 157 | datepicker_options[:day_names_short] ||= [_('Sun'), _('Mon'), _('Tue'), _('Wed'), _('Thu'), _('Fri'), _('Sat')] |
... | ... | @@ -236,7 +236,7 @@ module FormsHelper |
236 | 236 | weekHeader: #{datepicker_options[:week_header].to_json}, |
237 | 237 | yearRange: #{datepicker_options[:year_range].to_json}, |
238 | 238 | yearSuffix: #{datepicker_options[:year_suffix].to_json} |
239 | - }) | |
239 | + }).datepicker('setDate', new Date('#{value}')) | |
240 | 240 | </script> |
241 | 241 | ".html_safe |
242 | 242 | result | ... | ... |
app/helpers/layout_helper.rb
app/models/article.rb
... | ... | @@ -9,7 +9,7 @@ class Article < ActiveRecord::Base |
9 | 9 | :highlighted, :notify_comments, :display_hits, :slug, |
10 | 10 | :external_feed_builder, :display_versions, :external_link, |
11 | 11 | :image_builder, :show_to_followers, |
12 | - :author | |
12 | + :author, :display_preview | |
13 | 13 | |
14 | 14 | acts_as_having_image |
15 | 15 | |
... | ... | @@ -637,6 +637,20 @@ class Article < ActiveRecord::Base |
637 | 637 | can_display_hits? && display_hits |
638 | 638 | end |
639 | 639 | |
640 | + def display_media_panel? | |
641 | + can_display_media_panel? && environment.enabled?('media_panel') | |
642 | + end | |
643 | + | |
644 | + def can_display_media_panel? | |
645 | + false | |
646 | + end | |
647 | + | |
648 | + settings_items :display_preview, :type => :boolean, :default => false | |
649 | + | |
650 | + def display_preview? | |
651 | + false | |
652 | + end | |
653 | + | |
640 | 654 | def image? |
641 | 655 | false |
642 | 656 | end |
... | ... | @@ -745,9 +759,10 @@ class Article < ActiveRecord::Base |
745 | 759 | end |
746 | 760 | |
747 | 761 | def body_images_paths |
748 | - require 'uri' | |
749 | 762 | Nokogiri::HTML.fragment(self.body.to_s).css('img[src]').collect do |i| |
750 | - (self.profile && self.profile.environment) ? URI.join(self.profile.environment.top_url, URI.escape(i['src'])).to_s : i['src'] | |
763 | + src = i['src'] | |
764 | + src = URI.escape src if self.new_record? # xss_terminate runs on save | |
765 | + (self.profile && self.profile.environment) ? URI.join(self.profile.environment.top_url, src).to_s : src | |
751 | 766 | end |
752 | 767 | end |
753 | 768 | ... | ... |
app/models/category.rb
... | ... | @@ -81,7 +81,7 @@ class Category < ActiveRecord::Base |
81 | 81 | end |
82 | 82 | |
83 | 83 | def upcoming_events(limit = 10) |
84 | - self.events.where('start_date >= ?', Date.today).reorder('start_date').paginate(:page => 1, :per_page => limit) | |
84 | + self.events.where('start_date >= ?', DateTime.now.beginning_of_day).order('start_date').paginate(page: 1, per_page: limit) | |
85 | 85 | end |
86 | 86 | |
87 | 87 | def display_in_menu? | ... | ... |
app/models/enterprise_homepage.rb
app/models/environment.rb
... | ... | @@ -13,7 +13,7 @@ class Environment < ActiveRecord::Base |
13 | 13 | :reports_lower_bound, :noreply_email, |
14 | 14 | :signup_welcome_screen_body, :members_whitelist_enabled, |
15 | 15 | :members_whitelist, :highlighted_news_amount, |
16 | - :portal_news_amount, :date_format | |
16 | + :portal_news_amount, :date_format, :signup_intro | |
17 | 17 | |
18 | 18 | has_many :users |
19 | 19 | ... | ... |
app/models/event.rb
... | ... | @@ -23,7 +23,7 @@ class Event < Article |
23 | 23 | |
24 | 24 | def initialize(*args) |
25 | 25 | super(*args) |
26 | - self.start_date ||= Date.today | |
26 | + self.start_date ||= DateTime.now | |
27 | 27 | end |
28 | 28 | |
29 | 29 | validates_presence_of :title, :start_date |
... | ... | @@ -35,8 +35,9 @@ class Event < Article |
35 | 35 | end |
36 | 36 | |
37 | 37 | scope :by_day, -> (date) { |
38 | + where('start_date >= :start_date AND start_date <= :end_date AND end_date IS NULL OR (start_date <= :end_date AND end_date >= :start_date)', | |
39 | + {:start_date => date.beginning_of_day, :end_date => date.end_of_day}). | |
38 | 40 | order('start_date ASC') |
39 | - .where('start_date = :date AND end_date IS NULL OR (start_date <= :date AND end_date >= :date)', {:date => date}) | |
40 | 41 | } |
41 | 42 | |
42 | 43 | scope :next_events_from_month, -> (date) { |
... | ... | @@ -75,7 +76,7 @@ class Event < Article |
75 | 76 | |
76 | 77 | def self.date_range(year, month) |
77 | 78 | if year.nil? || month.nil? |
78 | - today = Date.today | |
79 | + today = DateTime.now | |
79 | 80 | year = today.year |
80 | 81 | month = today.month |
81 | 82 | else |
... | ... | @@ -83,7 +84,7 @@ class Event < Article |
83 | 84 | month = month.to_i |
84 | 85 | end |
85 | 86 | |
86 | - first_day = Date.new(year, month, 1) | |
87 | + first_day = DateTime.new(year, month, 1) | |
87 | 88 | last_day = first_day + 1.month - 1.day |
88 | 89 | |
89 | 90 | first_day..last_day |
... | ... | @@ -109,7 +110,7 @@ class Event < Article |
109 | 110 | end |
110 | 111 | |
111 | 112 | def duration |
112 | - ((self.end_date || self.start_date) - self.start_date).to_i | |
113 | + (((self.end_date || self.start_date) - self.start_date).to_i/60/60/24) | |
113 | 114 | end |
114 | 115 | |
115 | 116 | alias_method :article_lead, :lead |
... | ... | @@ -129,6 +130,10 @@ class Event < Article |
129 | 130 | true |
130 | 131 | end |
131 | 132 | |
133 | + def can_display_media_panel? | |
134 | + true | |
135 | + end | |
136 | + | |
132 | 137 | include Noosfero::TranslatableContent |
133 | 138 | include MaybeAddHttp |
134 | 139 | ... | ... |
app/models/profile.rb
... | ... | @@ -579,6 +579,14 @@ class Profile < ActiveRecord::Base |
579 | 579 | options.merge(Noosfero.url_options) |
580 | 580 | end |
581 | 581 | |
582 | + def top_url(scheme = 'http') | |
583 | + url = scheme + '://' | |
584 | + url << url_options[:host] | |
585 | + url << ':' << url_options[:port].to_s if url_options.key?(:port) | |
586 | + url << Noosfero.root('') | |
587 | + url | |
588 | + end | |
589 | + | |
582 | 590 | private :generate_url, :url_options |
583 | 591 | |
584 | 592 | def default_hostname | ... | ... |
app/models/text_article.rb
... | ... | @@ -33,12 +33,16 @@ class TextArticle < Article |
33 | 33 | end |
34 | 34 | |
35 | 35 | def change_element_path(el, attribute) |
36 | - fullpath = /(https?):\/\/(#{environment.default_hostname})(:\d+)?(\/.*)/.match(el[attribute]) | |
36 | + fullpath = /(https?):\/\/(#{profile.default_hostname})(:\d+)?(\/.*)/.match(el[attribute]) | |
37 | 37 | if fullpath |
38 | 38 | domain = fullpath[2] |
39 | 39 | path = fullpath[4] |
40 | - el[attribute] = path if domain == environment.default_hostname | |
40 | + el[attribute] = path if domain == profile.default_hostname | |
41 | 41 | end |
42 | 42 | end |
43 | 43 | |
44 | + def display_preview? | |
45 | + parent && parent.kind_of?(Blog) && parent.display_preview | |
46 | + end | |
47 | + | |
44 | 48 | end | ... | ... |
app/models/textile_article.rb
app/models/tiny_mce_article.rb
app/models/user.rb
... | ... | @@ -121,11 +121,17 @@ class User < ActiveRecord::Base |
121 | 121 | |
122 | 122 | validates_inclusion_of :terms_accepted, :in => [ '1' ], :if => lambda { |u| ! u.terms_of_use.blank? }, :message => N_('{fn} must be checked in order to signup.').fix_i18n |
123 | 123 | |
124 | + scope :has_login?, lambda { |login,email,environment_id| | |
125 | + where('login = ? OR email = ?', login, email). | |
126 | + where(environment_id: environment_id) | |
127 | + } | |
128 | + | |
124 | 129 | # Authenticates a user by their login name or email and unencrypted password. Returns the user or nil. |
125 | 130 | def self.authenticate(login, password, environment = nil) |
126 | 131 | environment ||= Environment.default |
127 | - u = self.where('(login = ? OR email = ?) AND environment_id = ? AND activated_at IS NOT NULL', | |
128 | - login, login, environment.id).first # need to get the salt | |
132 | + | |
133 | + u = self.has_login?(login, login, environment.id) | |
134 | + u = u.first if u.is_a?(ActiveRecord::Relation) | |
129 | 135 | u && u.authenticated?(password) ? u : nil |
130 | 136 | end |
131 | 137 | |
... | ... | @@ -237,7 +243,23 @@ class User < ActiveRecord::Base |
237 | 243 | password.crypt(salt) |
238 | 244 | end |
239 | 245 | |
246 | + class UserNotActivated < StandardError | |
247 | + attr_reader :user | |
248 | + | |
249 | + def initialize(message, user = nil) | |
250 | + @user = user | |
251 | + | |
252 | + super(message) | |
253 | + end | |
254 | + end | |
255 | + | |
240 | 256 | def authenticated?(password) |
257 | + | |
258 | + unless self.activated? | |
259 | + message = _('The user "%{login}" is not activated! Please check your email to activate your user') % {login: self.login} | |
260 | + raise UserNotActivated.new(message, self) | |
261 | + end | |
262 | + | |
241 | 263 | result = (crypted_password == encrypt(password)) |
242 | 264 | if (encryption_method != User.system_encryption_method) && result |
243 | 265 | self.password_type = User.system_encryption_method.to_s |
... | ... | @@ -276,9 +298,15 @@ class User < ActiveRecord::Base |
276 | 298 | # current password. |
277 | 299 | # * Saves the record unless it is a new one. |
278 | 300 | def change_password!(current, new, confirmation) |
279 | - unless self.authenticated?(current) | |
280 | - self.errors.add(:current_password, _('does not match.')) | |
281 | - raise IncorrectPassword | |
301 | + | |
302 | + begin | |
303 | + unless self.authenticated?(current) | |
304 | + self.errors.add(:current_password, _('does not match.')) | |
305 | + raise IncorrectPassword | |
306 | + end | |
307 | + rescue UserNotActivated => e | |
308 | + self.errors.add(:current_password, e.message) | |
309 | + raise UserNotActivated | |
282 | 310 | end |
283 | 311 | self.force_change_password!(new, confirmation) |
284 | 312 | end | ... | ... |
app/views/blocks/profile_info_actions/_join_leave_community.html.erb
... | ... | @@ -11,7 +11,7 @@ |
11 | 11 | class: 'join-community', |
12 | 12 | style: 'position: relative; display: none;' %> |
13 | 13 | <% else %> |
14 | - <%= button :add, _('Join this community'), profile.join_url %> | |
14 | + <%= button :add, _('Join this community'), profile.join_url, class: 'join-community' %> | |
15 | 15 | <% end %> |
16 | 16 | <% end %> |
17 | 17 | <% else %> | ... | ... |
app/views/cms/_event.html.erb
... | ... | @@ -8,9 +8,8 @@ |
8 | 8 | <%= render :partial => 'general_fields' %> |
9 | 9 | <%= render :partial => 'translatable' %> |
10 | 10 | |
11 | -<%= labelled_form_field(_('Start date'), pick_date(:article, :start_date)) %> | |
11 | +<%= date_range_field('article[start_date]', 'article[end_date]', @article.start_date, @article.end_date, _('%Y-%m-%d %H:%M'), {:time => true}, {:id => 'article_start_date'} ) %> | |
12 | 12 | |
13 | -<%= labelled_form_field(_('End date'), pick_date(:article, :end_date)) %> | |
14 | 13 | |
15 | 14 | <%= labelled_form_field(_('Event website:'), text_field(:article, :link)) %> |
16 | 15 | ... | ... |
app/views/cms/edit.html.erb
1 | 1 | <%= error_messages_for 'article' %> |
2 | 2 | |
3 | -<% show_media_panel = environment.enabled?('media_panel') && [TinyMceArticle, TextileArticle, Event, EnterpriseHomepage].any?{|klass| @article.kind_of?(klass)} %> | |
4 | - | |
5 | -<div class='<%= (show_media_panel ? 'with_media_panel' : 'no_media_panel') %>'> | |
3 | +<div class='<%= (@article.display_media_panel? ? 'with_media_panel' : 'no_media_panel') %>'> | |
6 | 4 | <%= labelled_form_for 'article', :html => { :multipart => true, :class => @type } do |f| %> |
7 | 5 | |
8 | 6 | <%= hidden_field_tag("type", @type) if @type %> |
... | ... | @@ -68,7 +66,7 @@ |
68 | 66 | <% end %> |
69 | 67 | </div> |
70 | 68 | |
71 | -<% if show_media_panel %> | |
69 | +<% if @article.display_media_panel? %> | |
72 | 70 | <%= render :partial => 'text_editor_sidebar' %> |
73 | 71 | <% end %> |
74 | 72 | ... | ... |
app/views/content_viewer/_article_title.html.erb
app/views/content_viewer/_publishing_info.html.erb
app/views/content_viewer/_uploaded_file.html.erb
... | ... | @@ -2,7 +2,7 @@ |
2 | 2 | <%= link_to '', |
3 | 3 | uploaded_file.view_url, |
4 | 4 | :class => 'image', |
5 | - :style => 'background-image: url(%s)'% uploaded_file.public_filename(:thumb) | |
5 | + :style => 'background-image: url(%s)'% [Noosfero.root, uploaded_file.public_filename(:thumb)].join | |
6 | 6 | %> |
7 | 7 | <span><%=h uploaded_file.title %></span> |
8 | 8 | <% else %> | ... | ... |
db/migrate/20150722042714_change_article_date_to_datetime.rb
0 → 100644
... | ... | @@ -0,0 +1,27 @@ |
1 | +class ChangeArticleDateToDatetime < ActiveRecord::Migration | |
2 | + | |
3 | + def up | |
4 | + change_table :articles do |t| | |
5 | + t.change :start_date, :datetime | |
6 | + t.change :end_date, :datetime | |
7 | + end | |
8 | + | |
9 | + change_table :article_versions do |t| | |
10 | + t.change :start_date, :datetime | |
11 | + t.change :end_date, :datetime | |
12 | + end | |
13 | + end | |
14 | + | |
15 | + def down | |
16 | + change_table :articles do |t| | |
17 | + t.change :start_date, :date | |
18 | + t.change :end_date, :date | |
19 | + end | |
20 | + | |
21 | + change_table :article_versions do |t| | |
22 | + t.change :start_date, :date | |
23 | + t.change :end_date, :date | |
24 | + end | |
25 | + end | |
26 | + | |
27 | +end | ... | ... |
db/schema.rb
... | ... | @@ -11,7 +11,7 @@ |
11 | 11 | # |
12 | 12 | # It's strongly recommended to check this file into your version control system. |
13 | 13 | |
14 | -ActiveRecord::Schema.define(:version => 20150712130827) do | |
14 | +ActiveRecord::Schema.define(:version => 20150722042714) do | |
15 | 15 | |
16 | 16 | create_table "abuse_reports", :force => true do |t| |
17 | 17 | t.integer "reporter_id" |
... | ... | @@ -75,8 +75,8 @@ ActiveRecord::Schema.define(:version => 20150712130827) do |
75 | 75 | t.integer "comments_count" |
76 | 76 | t.boolean "advertise", :default => true |
77 | 77 | t.boolean "published", :default => true |
78 | - t.date "start_date" | |
79 | - t.date "end_date" | |
78 | + t.datetime "start_date" | |
79 | + t.datetime "end_date" | |
80 | 80 | t.integer "children_count", :default => 0 |
81 | 81 | t.boolean "accept_comments", :default => true |
82 | 82 | t.integer "reference_article_id" |
... | ... | @@ -127,8 +127,8 @@ ActiveRecord::Schema.define(:version => 20150712130827) do |
127 | 127 | t.integer "comments_count", :default => 0 |
128 | 128 | t.boolean "advertise", :default => true |
129 | 129 | t.boolean "published", :default => true |
130 | - t.date "start_date" | |
131 | - t.date "end_date" | |
130 | + t.datetime "start_date" | |
131 | + t.datetime "end_date" | |
132 | 132 | t.integer "children_count", :default => 0 |
133 | 133 | t.boolean "accept_comments", :default => true |
134 | 134 | t.integer "reference_article_id" | ... | ... |
features/events.feature
... | ... | @@ -223,7 +223,7 @@ Feature: events |
223 | 223 | | owner | name | start_date | end_date | |
224 | 224 | | josesilva | WikiSym 2009 | 2009-10-25 | 2009-10-27 | |
225 | 225 | When I am on /profile/josesilva/events/2009/10/26 |
226 | - Then I should see "October 25, 2009 to October 27, 2009" | |
226 | + Then I should see "October 25, 2009 0:00 to October 27, 2009 0:00" | |
227 | 227 | |
228 | 228 | Scenario: show place of the event |
229 | 229 | Given I am on /profile/josesilva/events/2009/10 | ... | ... |
lib/tasks/release.rake
... | ... | @@ -239,12 +239,12 @@ EOF |
239 | 239 | end |
240 | 240 | end |
241 | 241 | |
242 | - Rake::Task['noosfero:upload'].invoke | |
243 | 242 | if confirm('Upload the packages') |
244 | 243 | puts "==> Uploading debian packages..." |
245 | 244 | Rake::Task['noosfero:upload_packages'].invoke(target) |
246 | 245 | else |
247 | - puts "I: please upload the package manually!" | |
246 | + puts "I: please upload the package manually later by running" | |
247 | + puts "I: $ rake noosfero:upload_packages" | |
248 | 248 | end |
249 | 249 | |
250 | 250 | rm_f "tmp/pending-release" | ... | ... |
plugins/analytics/controllers/myprofile/analytics_plugin/stats_controller.rb
0 → 100644
... | ... | @@ -0,0 +1,21 @@ |
1 | +class AnalyticsPlugin::StatsController < MyProfileController | |
2 | + | |
3 | + no_design_blocks | |
4 | + | |
5 | + before_filter :skip_page_view | |
6 | + | |
7 | + def index | |
8 | + end | |
9 | + | |
10 | + protected | |
11 | + | |
12 | + def default_url_options | |
13 | + # avoid rails' use_relative_controller! | |
14 | + {use_route: '/'} | |
15 | + end | |
16 | + | |
17 | + def skip_page_view | |
18 | + @analytics_skip_page_view = true | |
19 | + end | |
20 | + | |
21 | +end | ... | ... |
plugins/analytics/lib/analytics_plugin.rb
plugins/analytics/lib/analytics_plugin/base.rb
... | ... | @@ -38,4 +38,12 @@ class AnalyticsPlugin::Base < Noosfero::Plugin |
38 | 38 | }] |
39 | 39 | end |
40 | 40 | |
41 | + def control_panel_buttons | |
42 | + { | |
43 | + title: I18n.t('analytics_plugin.lib.plugin.panel_button'), | |
44 | + icon: 'analytics-access', | |
45 | + url: {controller: 'analytics_plugin/stats', action: :index} | |
46 | + } | |
47 | + end | |
48 | + | |
41 | 49 | end | ... | ... |
plugins/analytics/lib/ext/profile.rb
... | ... | @@ -13,7 +13,7 @@ end |
13 | 13 | class Profile |
14 | 14 | |
15 | 15 | def analytics_settings attrs = {} |
16 | - @analytics_settings ||= Noosfero::Plugin::Settings.new self, AnalyticsPlugin, attrs | |
16 | + @analytics_settings ||= Noosfero::Plugin::Settings.new self, ::AnalyticsPlugin, attrs | |
17 | 17 | attrs.each{ |a, v| @analytics_settings.send "#{a}=", v } |
18 | 18 | @analytics_settings |
19 | 19 | end | ... | ... |
plugins/analytics/locales/en.yml
... | ... | @@ -5,6 +5,13 @@ en: &en |
5 | 5 | plugin: |
6 | 6 | name: 'Access tracking' |
7 | 7 | description: 'Register the access of selected profiles' |
8 | + panel_button: 'Access tracking' | |
9 | + | |
10 | + views: | |
11 | + stats: | |
12 | + user: 'User' | |
13 | + initial_time: 'Time' | |
14 | + pages: 'Pages' | |
8 | 15 | |
9 | 16 | en-US: |
10 | 17 | <<: *en | ... | ... |
plugins/analytics/locales/pt.yml
... | ... | @@ -5,6 +5,13 @@ pt: &pt |
5 | 5 | plugin: |
6 | 6 | name: 'Rastreio de accesso' |
7 | 7 | description: 'Registra o acesso de perfis selecionados' |
8 | + panel_button: 'Rastreio de accesso' | |
9 | + | |
10 | + views: | |
11 | + stats: | |
12 | + user: 'Usuário' | |
13 | + initial_time: 'Horário' | |
14 | + pages: 'Páginas' | |
8 | 15 | |
9 | 16 | pt-BR: |
10 | 17 | <<: *pt | ... | ... |
plugins/analytics/models/analytics_plugin/page_view.rb
... | ... | @@ -25,10 +25,24 @@ class AnalyticsPlugin::PageView < ActiveRecord::Base |
25 | 25 | before_validation :fill_referer_page_view, on: :create |
26 | 26 | before_validation :fill_visit, on: :create |
27 | 27 | |
28 | + scope :latest, -> { order 'request_started_at DESC' } | |
29 | + | |
28 | 30 | def request_duration |
29 | 31 | self.request_finished_at - self.request_started_at |
30 | 32 | end |
31 | 33 | |
34 | + def initial_time | |
35 | + self.page_loaded_at || self.request_finished_at | |
36 | + end | |
37 | + | |
38 | + def user_last_time_seen | |
39 | + self.initial_time + self.time_on_page | |
40 | + end | |
41 | + | |
42 | + def user_on_page? | |
43 | + Time.now < self.user_last_time_seen + AnalyticsPlugin::TimeOnPageUpdateInterval | |
44 | + end | |
45 | + | |
32 | 46 | def page_load! |
33 | 47 | self.page_loaded_at = Time.now |
34 | 48 | self.update_column :page_loaded_at, self.page_loaded_at |
... | ... | @@ -36,10 +50,9 @@ class AnalyticsPlugin::PageView < ActiveRecord::Base |
36 | 50 | |
37 | 51 | def increase_time_on_page! |
38 | 52 | now = Time.now |
39 | - initial_time = self.page_loaded_at || self.request_finished_at | |
40 | - return unless now > initial_time | |
53 | + return unless now > self.initial_time | |
41 | 54 | |
42 | - self.time_on_page = now - initial_time | |
55 | + self.time_on_page = now - self.initial_time | |
43 | 56 | self.update_column :time_on_page, self.time_on_page |
44 | 57 | end |
45 | 58 | |
... | ... | @@ -59,7 +72,7 @@ class AnalyticsPlugin::PageView < ActiveRecord::Base |
59 | 72 | end |
60 | 73 | |
61 | 74 | def fill_visit |
62 | - self.visit = self.referer_page_view.visit if self.referer_page_view | |
75 | + self.visit = self.referer_page_view.visit if self.referer_page_view and self.referer_page_view.user_on_page? | |
63 | 76 | self.visit ||= AnalyticsPlugin::Visit.new profile: profile |
64 | 77 | end |
65 | 78 | ... | ... |
plugins/analytics/models/analytics_plugin/visit.rb
... | ... | @@ -3,9 +3,17 @@ class AnalyticsPlugin::Visit < ActiveRecord::Base |
3 | 3 | attr_accessible *self.column_names |
4 | 4 | attr_accessible :profile |
5 | 5 | |
6 | - default_scope -> { includes :page_views } | |
7 | - | |
8 | 6 | belongs_to :profile |
9 | 7 | has_many :page_views, class_name: 'AnalyticsPlugin::PageView', dependent: :destroy |
10 | 8 | |
9 | + default_scope -> { joins(:page_views).includes :page_views } | |
10 | + | |
11 | + scope :latest, -> { order 'analytics_plugin_page_views.request_started_at DESC' } | |
12 | + | |
13 | + def first_page_view | |
14 | + self.page_views.first | |
15 | + end | |
16 | + | |
17 | + delegate :user, :initial_time, to: :first_page_view | |
18 | + | |
11 | 19 | end | ... | ... |
plugins/analytics/test/functional/content_viewer_controller_test.rb
... | ... | @@ -31,6 +31,8 @@ class ContentViewerControllerTest < ActionController::TestCase |
31 | 31 | |
32 | 32 | first_page_view = @community.page_views.order(:id).first |
33 | 33 | assert_equal @request.referer, first_page_view.referer_url |
34 | + assert_equal @user, first_page_view.user | |
35 | + assert first_page_view.request_duration > 0 and first_page_view.request_duration < 1 | |
34 | 36 | |
35 | 37 | @request.env['HTTP_REFERER'] = first_url |
36 | 38 | get :view_page, profile: @community.identifier, page: @community.articles.last.path.split('/') |
... | ... | @@ -40,9 +42,13 @@ class ContentViewerControllerTest < ActionController::TestCase |
40 | 42 | second_page_view = @community.page_views.order(:id).last |
41 | 43 | assert_equal first_page_view, second_page_view.referer_page_view |
42 | 44 | |
43 | - assert_equal @user, second_page_view.user | |
44 | - | |
45 | - assert second_page_view.request_duration > 0 and second_page_view.request_duration < 1 | |
45 | + # another visit, the referer is set but should be ignored because | |
46 | + # the user didn't report to be on the page until now | |
47 | + @request.env['HTTP_REFERER'] = first_url | |
48 | + future = Time.now + 2*AnalyticsPlugin::TimeOnPageUpdateInterval | |
49 | + Time.stubs(:now).returns(future) | |
50 | + get :view_page, profile: @community.identifier, page: @community.articles.last.path.split('/') | |
51 | + assert_equal 2, @community.visits.count | |
46 | 52 | end |
47 | 53 | |
48 | 54 | end | ... | ... |
plugins/analytics/views/analytics_plugin/_body_ending.html.slim
1 | 1 | javascript: |
2 | 2 | analytics.timeOnPage.baseUrl = #{url_for(controller: 'analytics_plugin/time_on_page').to_json} |
3 | - analytics.timeOnPage.updateInterval = #{AnalyticsPlugin::TimeOnPageUpdateInterval.to_json} | |
3 | + analytics.timeOnPage.updateInterval = #{AnalyticsPlugin::TimeOnPageUpdateIntervalMs.to_json} | |
4 | 4 | analytics.requestId = #{request.env['action_dispatch.request_id'].to_json} |
5 | 5 | analytics.init() |
6 | 6 | ... | ... |
plugins/analytics/views/analytics_plugin/stats/_table.html.slim
0 → 100644
... | ... | @@ -0,0 +1,38 @@ |
1 | + | |
2 | +table#analytics-stats.table data-toggle='table' data-striped='true' data-sortable='true' data-icons-prefix='fa' | |
3 | + thead | |
4 | + - unless profile.analytics_anonymous? | |
5 | + th= t'analytics_plugin.views.stats.user' | |
6 | + th= t'analytics_plugin.views.stats.initial_time' | |
7 | + th= t'analytics_plugin.views.stats.pages' | |
8 | + | |
9 | + tbody | |
10 | + - profile.visits.each do |visit| | |
11 | + tr | |
12 | + td= link_to visit.user.name, visit.user.url | |
13 | + td | |
14 | + div data-toggle="tooltip" data-title='#{l visit.initial_time}' | |
15 | + = time_ago_in_words(visit.initial_time) | |
16 | + |  | |
17 | + = _'ago' | |
18 | + td | |
19 | + - visit.page_views.each do |page_view| | |
20 | + = link_to page_view.url, page_view.url | |
21 | + | | |
22 | + = "(#{distance_of_time_in_words page_view.time_on_page})" | |
23 | + | -> | |
24 | + | |
25 | +javascript: | |
26 | + $('#analytics-stats').bootstrapTable({ | |
27 | + striped: true, | |
28 | + columns: [ | |
29 | + {sortable: true}, | |
30 | + {sortable: true}, | |
31 | + {sortable: true}, | |
32 | + ], | |
33 | + }) | |
34 | + | |
35 | + $(document).ready(function() { | |
36 | + $('[data-toggle="tooltip"]').tooltip() | |
37 | + }) | |
38 | + | ... | ... |
plugins/analytics/views/analytics_plugin/stats/index.html.slim
0 → 100644
... | ... | @@ -0,0 +1,5 @@ |
1 | +- content_for :head | |
2 | + = javascript_include_tag 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.8.1/bootstrap-table-all.min.js' | |
3 | + = stylesheet_link_tag 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.8.1/bootstrap-table.css' | |
4 | + | |
5 | += render 'table' | ... | ... |
plugins/community_track/lib/community_track_plugin/step.rb
... | ... | @@ -29,8 +29,8 @@ class CommunityTrackPlugin::Step < Folder |
29 | 29 | |
30 | 30 | def initialize(*args) |
31 | 31 | super(*args) |
32 | - self.start_date ||= Date.today | |
33 | - self.end_date ||= Date.today + 1.day | |
32 | + self.start_date ||= DateTime.now | |
33 | + self.end_date ||= DateTime.now + 1.day | |
34 | 34 | end |
35 | 35 | |
36 | 36 | def set_hidden_position |
... | ... | @@ -72,20 +72,20 @@ class CommunityTrackPlugin::Step < Folder |
72 | 72 | end |
73 | 73 | |
74 | 74 | def active? |
75 | - (start_date..end_date).include?(Date.today) | |
75 | + (start_date..end_date).cover?(DateTime.now) | |
76 | 76 | end |
77 | 77 | |
78 | 78 | def finished? |
79 | - Date.today > end_date | |
79 | + DateTime.now > end_date | |
80 | 80 | end |
81 | 81 | |
82 | 82 | def waiting? |
83 | - Date.today < start_date | |
83 | + DateTime.now < start_date | |
84 | 84 | end |
85 | 85 | |
86 | 86 | def schedule_activation |
87 | 87 | return if !changes['start_date'] && !changes['end_date'] |
88 | - if Date.today <= end_date || accept_comments | |
88 | + if DateTime.now <= end_date || accept_comments | |
89 | 89 | schedule_date = !accept_comments ? start_date : end_date + 1.day |
90 | 90 | CommunityTrackPlugin::ActivationJob.find(id).destroy_all |
91 | 91 | Delayed::Job.enqueue(CommunityTrackPlugin::ActivationJob.new(self.id), :run_at => schedule_date) | ... | ... |
plugins/community_track/test/functional/community_track_plugin_content_viewer_controller_test.rb
... | ... | @@ -5,7 +5,7 @@ class ContentViewerControllerTest < ActionController::TestCase |
5 | 5 | def setup |
6 | 6 | @profile = Community.create!(:name => 'Sample community', :identifier => 'sample-community') |
7 | 7 | @track = create_track('track', @profile) |
8 | - @step = CommunityTrackPlugin::Step.create!(:name => 'step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today, :tool_type => TinyMceArticle.name) | |
8 | + @step = CommunityTrackPlugin::Step.create!(:name => 'step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => DateTime.now.end_of_day, :start_date => DateTime.now.beginning_of_day, :tool_type => TinyMceArticle.name) | |
9 | 9 | |
10 | 10 | user = create_user('testinguser') |
11 | 11 | login_as(user.login) | ... | ... |
plugins/community_track/test/unit/community_track_plugin/step_test.rb
... | ... | @@ -9,7 +9,7 @@ class StepTest < ActiveSupport::TestCase |
9 | 9 | @track.add_category(@category) |
10 | 10 | @track.save! |
11 | 11 | |
12 | - @step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today) | |
12 | + @step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => DateTime.now.end_of_day, :start_date => DateTime.now.beginning_of_day - 1.day) | |
13 | 13 | Delayed::Job.destroy_all |
14 | 14 | end |
15 | 15 | |
... | ... | @@ -22,39 +22,39 @@ class StepTest < ActiveSupport::TestCase |
22 | 22 | end |
23 | 23 | |
24 | 24 | should 'set accept_comments to false on create' do |
25 | - today = Date.today | |
25 | + today = DateTime.now | |
26 | 26 | step = CommunityTrackPlugin::Step.create(:name => 'Step', :body => 'body', :profile => @profile, :parent => @track, :start_date => today, :end_date => today, :published => true) |
27 | 27 | refute step.accept_comments |
28 | 28 | end |
29 | 29 | |
30 | 30 | should 'do not allow step creation with a parent that is not a track' do |
31 | - today = Date.today | |
31 | + today = DateTime.now | |
32 | 32 | blog = fast_create(Blog) |
33 | 33 | step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => blog, :start_date => today, :end_date => today, :published => true) |
34 | 34 | refute step.save |
35 | 35 | end |
36 | 36 | |
37 | 37 | should 'do not allow step creation without a parent' do |
38 | - today = Date.today | |
38 | + today = DateTime.now | |
39 | 39 | step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => nil, :start_date => today, :end_date => today, :published => true) |
40 | 40 | refute step.save |
41 | 41 | end |
42 | 42 | |
43 | 43 | should 'create step if end date is equal to start date' do |
44 | - @step.start_date = Date.today | |
45 | - @step.end_date = Date.today | |
44 | + @step.start_date = DateTime.now | |
45 | + @step.end_date = DateTime.now | |
46 | 46 | assert @step.save |
47 | 47 | end |
48 | 48 | |
49 | 49 | should 'create step if end date is after start date' do |
50 | - @step.start_date = Date.today | |
51 | - @step.end_date = Date.today + 1.day | |
50 | + @step.start_date = DateTime.now | |
51 | + @step.end_date = DateTime.now + 1.day | |
52 | 52 | assert @step.save |
53 | 53 | end |
54 | 54 | |
55 | 55 | should 'do not create step if end date is before start date' do |
56 | - @step.start_date = Date.today | |
57 | - @step.end_date = Date.today - 1.day | |
56 | + @step.start_date = DateTime.now | |
57 | + @step.end_date = DateTime.now - 1.day | |
58 | 58 | refute @step.save |
59 | 59 | end |
60 | 60 | |
... | ... | @@ -71,20 +71,20 @@ class StepTest < ActiveSupport::TestCase |
71 | 71 | end |
72 | 72 | |
73 | 73 | should 'be active if today is between start and end dates' do |
74 | - @step.start_date = Date.today | |
75 | - @step.end_date = Date.today + 1.day | |
74 | + @step.start_date = DateTime.now | |
75 | + @step.end_date = DateTime.now + 1.day | |
76 | 76 | assert @step.active? |
77 | 77 | end |
78 | 78 | |
79 | 79 | should 'be finished if today is after the end date' do |
80 | - @step.start_date = Date.today - 2.day | |
81 | - @step.end_date = Date.today - 1.day | |
80 | + @step.start_date = DateTime.now - 2.day | |
81 | + @step.end_date = DateTime.now - 1.day | |
82 | 82 | assert @step.finished? |
83 | 83 | end |
84 | 84 | |
85 | 85 | should 'be waiting if today is before the end date' do |
86 | - @step.start_date = Date.today + 1.day | |
87 | - @step.end_date = Date.today + 2.day | |
86 | + @step.start_date = DateTime.now + 1.day | |
87 | + @step.end_date = DateTime.now + 2.day | |
88 | 88 | assert @step.waiting? |
89 | 89 | end |
90 | 90 | |
... | ... | @@ -95,17 +95,17 @@ class StepTest < ActiveSupport::TestCase |
95 | 95 | end |
96 | 96 | |
97 | 97 | should 'create delayed job' do |
98 | - @step.start_date = Date.today | |
99 | - @step.end_date = Date.today | |
98 | + @step.start_date = DateTime.now.beginning_of_day | |
99 | + @step.end_date = DateTime.now.end_of_day | |
100 | 100 | @step.accept_comments = false |
101 | 101 | @step.schedule_activation |
102 | 102 | assert_equal 1, Delayed::Job.count |
103 | - assert_equal @step.start_date, Delayed::Job.first.run_at.to_date | |
103 | + assert_equal @step.start_date, Delayed::Job.first.run_at | |
104 | 104 | end |
105 | 105 | |
106 | 106 | should 'do not duplicate delayed job' do |
107 | - @step.start_date = Date.today | |
108 | - @step.end_date = Date.today | |
107 | + @step.start_date = DateTime.now | |
108 | + @step.end_date = DateTime.now | |
109 | 109 | @step.schedule_activation |
110 | 110 | assert_equal 1, Delayed::Job.count |
111 | 111 | @step.schedule_activation |
... | ... | @@ -113,30 +113,30 @@ class StepTest < ActiveSupport::TestCase |
113 | 113 | end |
114 | 114 | |
115 | 115 | should 'create delayed job when a step is saved' do |
116 | - @step.start_date = Date.today | |
117 | - @step.end_date = Date.today | |
116 | + @step.start_date = DateTime.now.beginning_of_day | |
117 | + @step.end_date = DateTime.now.end_of_day | |
118 | 118 | @step.save! |
119 | - assert_equal @step.start_date, Delayed::Job.first.run_at.to_date | |
119 | + assert_equal @step.start_date, Delayed::Job.first.run_at | |
120 | 120 | end |
121 | 121 | |
122 | 122 | should 'create delayed job even if start date has passed' do |
123 | - @step.start_date = Date.today - 2.days | |
124 | - @step.end_date = Date.today | |
123 | + @step.start_date = DateTime.now - 2.days | |
124 | + @step.end_date = DateTime.now.end_of_day | |
125 | 125 | @step.accept_comments = false |
126 | 126 | @step.schedule_activation |
127 | - assert_equal @step.start_date, Delayed::Job.first.run_at.to_date | |
127 | + assert_equal @step.start_date, Delayed::Job.first.run_at | |
128 | 128 | end |
129 | 129 | |
130 | 130 | should 'create delayed job if end date has passed' do |
131 | - @step.start_date = Date.today - 5.days | |
132 | - @step.end_date = Date.today - 2.days | |
131 | + @step.start_date = DateTime.now - 5.days | |
132 | + @step.end_date = DateTime.now - 2.days | |
133 | 133 | @step.schedule_activation |
134 | - assert_equal @step.end_date + 1.day, Delayed::Job.first.run_at.to_date | |
134 | + assert_equal @step.end_date + 1.day, Delayed::Job.first.run_at | |
135 | 135 | end |
136 | 136 | |
137 | 137 | should 'do not schedule delayed job if save but do not modify date fields' do |
138 | - @step.start_date = Date.today | |
139 | - @step.end_date = Date.today | |
138 | + @step.start_date = DateTime.now | |
139 | + @step.end_date = DateTime.now.end_of_day | |
140 | 140 | @step.save! |
141 | 141 | assert_equal 1, Delayed::Job.count |
142 | 142 | Delayed::Job.destroy_all |
... | ... | @@ -149,13 +149,13 @@ class StepTest < ActiveSupport::TestCase |
149 | 149 | refute @step.position |
150 | 150 | @step.save! |
151 | 151 | assert_equal 1, @step.position |
152 | - step2 = CommunityTrackPlugin::Step.new(:name => 'Step2', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today) | |
152 | + step2 = CommunityTrackPlugin::Step.new(:name => 'Step2', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => DateTime.now.end_of_day, :start_date => DateTime.now.beginning_of_day) | |
153 | 153 | step2.save! |
154 | 154 | assert_equal 2, step2.position |
155 | 155 | end |
156 | 156 | |
157 | 157 | should 'accept comments if step is active' do |
158 | - @step.start_date = Date.today | |
158 | + @step.start_date = DateTime.now | |
159 | 159 | @step.save! |
160 | 160 | refute @step.accept_comments |
161 | 161 | @step.toggle_activation |
... | ... | @@ -164,8 +164,8 @@ class StepTest < ActiveSupport::TestCase |
164 | 164 | end |
165 | 165 | |
166 | 166 | should 'do not accept comments if step is not active' do |
167 | - @step.start_date = Date.today + 2.days | |
168 | - @step.end_date = Date.today + 3.days | |
167 | + @step.start_date = DateTime.now + 2.days | |
168 | + @step.end_date = DateTime.now + 3.days | |
169 | 169 | @step.save! |
170 | 170 | refute @step.published |
171 | 171 | @step.toggle_activation |
... | ... | @@ -174,14 +174,14 @@ class StepTest < ActiveSupport::TestCase |
174 | 174 | end |
175 | 175 | |
176 | 176 | should 'do not accept comments if step is not active anymore' do |
177 | - @step.start_date = Date.today | |
177 | + @step.end_date = DateTime.now.end_of_day | |
178 | 178 | @step.save! |
179 | 179 | @step.toggle_activation |
180 | 180 | @step.reload |
181 | 181 | assert @step.accept_comments |
182 | 182 | |
183 | - @step.start_date = Date.today - 2.days | |
184 | - @step.end_date = Date.today - 1.day | |
183 | + @step.start_date = DateTime.now - 2.days | |
184 | + @step.end_date = DateTime.now - 1.day | |
185 | 185 | @step.save! |
186 | 186 | @step.toggle_activation |
187 | 187 | @step.reload |
... | ... | @@ -203,7 +203,7 @@ class StepTest < ActiveSupport::TestCase |
203 | 203 | end |
204 | 204 | |
205 | 205 | should 'change position to botton if a hidden step becomes visible' do |
206 | - step1 = CommunityTrackPlugin::Step.new(:name => 'Step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today) | |
206 | + step1 = CommunityTrackPlugin::Step.new(:name => 'Step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => DateTime.now.end_of_day, :start_date => DateTime.now.beginning_of_day) | |
207 | 207 | step1.save! |
208 | 208 | @step.hidden = true |
209 | 209 | @step.save! |
... | ... | @@ -215,7 +215,7 @@ class StepTest < ActiveSupport::TestCase |
215 | 215 | |
216 | 216 | should 'decrement lower items positions if a step becomes hidden' do |
217 | 217 | @step.save! |
218 | - step1 = CommunityTrackPlugin::Step.new(:name => 'Step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today) | |
218 | + step1 = CommunityTrackPlugin::Step.new(:name => 'Step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => DateTime.now.end_of_day, :start_date => DateTime.now.beginning_of_day) | |
219 | 219 | step1.save! |
220 | 220 | assert_equal 2, step1.position |
221 | 221 | @step.hidden = true |
... | ... | @@ -225,7 +225,7 @@ class StepTest < ActiveSupport::TestCase |
225 | 225 | end |
226 | 226 | |
227 | 227 | should 'do not publish a hidden step' do |
228 | - @step.start_date = Date.today | |
228 | + @step.start_date = DateTime.now | |
229 | 229 | @step.hidden = true |
230 | 230 | @step.save! |
231 | 231 | refute @step.published |
... | ... | @@ -266,7 +266,7 @@ class StepTest < ActiveSupport::TestCase |
266 | 266 | end |
267 | 267 | |
268 | 268 | should 'enable comments on children when step is activated' do |
269 | - @step.start_date = Date.today | |
269 | + @step.start_date = DateTime.now | |
270 | 270 | @step.save! |
271 | 271 | refute @step.accept_comments |
272 | 272 | article = fast_create(Article, :parent_id => @step.id, :profile_id => @step.profile.id, :accept_comments => false) |
... | ... | @@ -276,8 +276,7 @@ class StepTest < ActiveSupport::TestCase |
276 | 276 | end |
277 | 277 | |
278 | 278 | should 'enable comments on children when step is active' do |
279 | - @step.start_date = Date.today | |
280 | - @step.start_date = Date.today | |
279 | + @step.start_date = DateTime.now | |
281 | 280 | @step.save! |
282 | 281 | refute @step.accept_comments |
283 | 282 | @step.toggle_activation | ... | ... |
plugins/event/lib/event_plugin/event_block.rb
... | ... | @@ -30,13 +30,13 @@ class EventPlugin::EventBlock < Block |
30 | 30 | events = user.nil? ? events.is_public : events.display_filter(user,nil) |
31 | 31 | |
32 | 32 | if future_only |
33 | - events = events.where('start_date >= ?', Date.today) | |
33 | + events = events.where('start_date >= ?', DateTime.now.beginning_of_day) | |
34 | 34 | end |
35 | 35 | |
36 | 36 | if date_distance_limit > 0 |
37 | 37 | events = events.by_range([ |
38 | - Date.today - date_distance_limit, | |
39 | - Date.today + date_distance_limit | |
38 | + DateTime.now.beginning_of_day - date_distance_limit, | |
39 | + DateTime.now.beginning_of_day + date_distance_limit | |
40 | 40 | ]) |
41 | 41 | end |
42 | 42 | ... | ... |
plugins/event/test/functional/event_block_test.rb
... | ... | @@ -7,7 +7,7 @@ class HomeControllerTest < ActionController::TestCase |
7 | 7 | @env.enable_plugin('EventPlugin') |
8 | 8 | |
9 | 9 | @p1 = fast_create(Person, :environment_id => @env.id) |
10 | - @e1a = fast_create(Event, :name=>'Event p1 A', :profile_id=>@p1.id) | |
10 | + @e1a = Event.create!(:name=>'Event p1 A', :profile =>@p1) | |
11 | 11 | |
12 | 12 | box = Box.create!(:owner => @env) |
13 | 13 | @block = EventPlugin::EventBlock.create!(:box => box) |
... | ... | @@ -19,6 +19,7 @@ class HomeControllerTest < ActionController::TestCase |
19 | 19 | |
20 | 20 | should 'see events microdata sturcture' do |
21 | 21 | get :index |
22 | +#raise response.body.inspect | |
22 | 23 | assert_select '.event-plugin_event-block ul.events' |
23 | 24 | assert_select ev |
24 | 25 | assert_select ev + 'a[itemprop="url"]' |
... | ... | @@ -33,15 +34,15 @@ class HomeControllerTest < ActionController::TestCase |
33 | 34 | |
34 | 35 | should 'see event duration' do |
35 | 36 | @e1a.slug = 'event1a' |
36 | - @e1a.start_date = Date.today | |
37 | - @e1a.end_date = Date.today + 1.day | |
37 | + @e1a.start_date = DateTime.now | |
38 | + @e1a.end_date = DateTime.now + 1.day | |
38 | 39 | @e1a.save! |
39 | 40 | get :index |
40 | 41 | assert_select ev + 'time.duration[itemprop="endDate"]', /1 day/ |
41 | 42 | |
42 | 43 | @e1a.slug = 'event1a' |
43 | - @e1a.start_date = Date.today | |
44 | - @e1a.end_date = Date.today + 2.day | |
44 | + @e1a.start_date = DateTime.now | |
45 | + @e1a.end_date = DateTime.now + 2.day | |
45 | 46 | @e1a.save! |
46 | 47 | get :index |
47 | 48 | assert_select ev + 'time.duration[itemprop="endDate"]', /2 days/ |
... | ... | @@ -52,8 +53,8 @@ class HomeControllerTest < ActionController::TestCase |
52 | 53 | assert_select ev + 'time.duration[itemprop="endDate"]', false |
53 | 54 | |
54 | 55 | @e1a.slug = 'event1a' |
55 | - @e1a.start_date = Date.today | |
56 | - @e1a.end_date = Date.today | |
56 | + @e1a.start_date = DateTime.now | |
57 | + @e1a.end_date = DateTime.now | |
57 | 58 | @e1a.save! |
58 | 59 | get :index |
59 | 60 | assert_select ev + 'time.duration[itemprop="endDate"]', false | ... | ... |
plugins/event/views/blocks/event.html.erb
... | ... | @@ -2,7 +2,7 @@ |
2 | 2 | |
3 | 3 | <ul class="events"> |
4 | 4 | <% block.events(user).map do |event| %> |
5 | - <% days_left = ( event.start_date - Date.today ).round %> | |
5 | + <% days_left = ( (event.start_date - DateTime.now)/60/60/24 ).round %> | |
6 | 6 | <li itemscope="itemscope" itemtype="http://data-vocabulary.org/Event" class="event"> |
7 | 7 | <%= render( |
8 | 8 | :file => 'event_plugin/event_block_item', | ... | ... |
... | ... | @@ -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} + '&' | ... | ... |