Commit c686ddb28d936095255c5792fcac75f271c43068

Authored by Braulio Bhavamitra
2 parents f3ac17d4 492c3d63

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

722 Bytes

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

3.31 KB

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

18.3 KB

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