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
app/controllers/public/account_controller.rb
@@ -46,8 +46,12 @@ class AccountController < ApplicationController @@ -46,8 +46,12 @@ class AccountController < ApplicationController
46 46
47 self.current_user = plugins_alternative_authentication 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 if logged_in? 55 if logged_in?
52 check_join_in_community(self.current_user) 56 check_join_in_community(self.current_user)
53 57
app/controllers/public/search_controller.rb
@@ -92,10 +92,10 @@ class SearchController < PublicController @@ -92,10 +92,10 @@ class SearchController < PublicController
92 92
93 def events 93 def events
94 if params[:year].blank? && params[:year].blank? && params[:day].blank? 94 if params[:year].blank? && params[:year].blank? && params[:day].blank?
95 - @date = Date.today 95 + @date = DateTime.now
96 else 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 day = (params[:day] ? params[:day].to_i : 1) 99 day = (params[:day] ? params[:day].to_i : 1)
100 @date = build_date(year, month, day) 100 @date = build_date(year, month, day)
101 end 101 end
@@ -106,9 +106,7 @@ class SearchController < PublicController @@ -106,9 +106,7 @@ class SearchController < PublicController
106 @events = @category ? 106 @events = @category ?
107 environment.events.by_day(@date).in_category(Category.find(@category_id)).paginate(:per_page => per_page, :page => params[:page]) : 107 environment.events.by_day(@date).in_category(Category.find(@category_id)).paginate(:per_page => per_page, :page => params[:page]) :
108 environment.events.by_day(@date).paginate(:per_page => per_page, :page => params[:page]) 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 @events = @category ? 110 @events = @category ?
113 environment.events.by_month(@date).in_category(Category.find(@category_id)).paginate(:per_page => per_page, :page => params[:page]) : 111 environment.events.by_month(@date).in_category(Category.find(@category_id)).paginate(:per_page => per_page, :page => params[:page]) :
114 environment.events.by_month(@date).paginate(:per_page => per_page, :page => params[:page]) 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,7 +903,7 @@ module ApplicationHelper
903 end 903 end
904 904
905 def base_url 905 def base_url
906 - environment.top_url(request.scheme) 906 + profile ? profile.top_url(request.scheme) : environment.top_url(request.scheme)
907 end 907 end
908 alias :top_url :base_url 908 alias :top_url :base_url
909 909
app/helpers/blog_helper.rb
@@ -6,7 +6,13 @@ module BlogHelper @@ -6,7 +6,13 @@ module BlogHelper
6 @article = article 6 @article = article
7 hidden_field_tag('article[published]', 1) + 7 hidden_field_tag('article[published]', 1) +
8 hidden_field_tag('article[accept_comments]', 0) + 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 end 16 end
11 17
12 def cms_label_for_new_children 18 def cms_label_for_new_children
app/helpers/content_viewer_helper.rb
@@ -51,7 +51,7 @@ module ContentViewerHelper @@ -51,7 +51,7 @@ module ContentViewerHelper
51 elsif date_format == 'past_time' 51 elsif date_format == 'past_time'
52 left_time = true 52 left_time = true
53 end 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 end 55 end
56 56
57 def link_to_comments(article, args = {}) 57 def link_to_comments(article, args = {})
app/helpers/dates_helper.rb
@@ -43,9 +43,14 @@ module DatesHelper @@ -43,9 +43,14 @@ module DatesHelper
43 end 43 end
44 44
45 # formats a datetime for displaying. 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 else 54 else
50 '' 55 ''
51 end 56 end
@@ -53,7 +58,7 @@ module DatesHelper @@ -53,7 +58,7 @@ module DatesHelper
53 58
54 def show_period(date1, date2 = nil, use_numbers = false) 59 def show_period(date1, date2 = nil, use_numbers = false)
55 if (date1 == date2) || (date2.nil?) 60 if (date1 == date2) || (date2.nil?)
56 - show_date(date1, use_numbers) 61 + show_time(date1, use_numbers)
57 else 62 else
58 if date1.year == date2.year 63 if date1.year == date2.year
59 if date1.month == date2.month 64 if date1.month == date2.month
@@ -72,8 +77,8 @@ module DatesHelper @@ -72,8 +77,8 @@ module DatesHelper
72 end 77 end
73 else 78 else
74 _('from %{date1} to %{date2}') % { 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 end 83 end
79 end 84 end
@@ -106,18 +111,18 @@ module DatesHelper @@ -106,18 +111,18 @@ module DatesHelper
106 111
107 def build_date(year, month, day = 1) 112 def build_date(year, month, day = 1)
108 if year.blank? and month.blank? and day.blank? 113 if year.blank? and month.blank? and day.blank?
109 - Date.today 114 + DateTime.now
110 else 115 else
111 if year.blank? 116 if year.blank?
112 - year = Date.today.year 117 + year = DateTime.now.year
113 end 118 end
114 if month.blank? 119 if month.blank?
115 - month = Date.today.month 120 + month = DateTime.now.month
116 end 121 end
117 if day.blank? 122 if day.blank?
118 day = 1 123 day = 1
119 end 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 end 126 end
122 end 127 end
123 128
app/helpers/events_helper.rb
@@ -16,7 +16,7 @@ module EventsHelper @@ -16,7 +16,7 @@ module EventsHelper
16 16
17 content_tag( 'tr', 17 content_tag( 'tr',
18 content_tag('td', 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 content_tag('div',link_to(article.name,article.url),:class => 'event-title') + 20 content_tag('div',link_to(article.name,article.url),:class => 'event-title') +
21 content_tag('div',(article.address.nil? or article.address == '') ? '' : (_('Place: ') + article.address),:class => 'event-place') 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,7 +30,7 @@ module EventsHelper
30 # the day itself 30 # the day itself
31 date, 31 date,
32 # is there any events in this date? 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 # is this date in the current month? 34 # is this date in the current month?
35 true 35 true
36 ] 36 ]
app/helpers/forms_helper.rb
@@ -151,7 +151,7 @@ module FormsHelper @@ -151,7 +151,7 @@ module FormsHelper
151 datepicker_options[:close_text] ||= _('Done') 151 datepicker_options[:close_text] ||= _('Done')
152 datepicker_options[:constrain_input] ||= true 152 datepicker_options[:constrain_input] ||= true
153 datepicker_options[:current_text] ||= _('Today') 153 datepicker_options[:current_text] ||= _('Today')
154 - datepicker_options[:date_format] ||= 'mm/dd/yy' 154 + datepicker_options[:date_format] ||= 'yy/mm/dd'
155 datepicker_options[:day_names] ||= [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')] 155 datepicker_options[:day_names] ||= [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')]
156 datepicker_options[:day_names_min] ||= [_('Su'), _('Mo'), _('Tu'), _('We'), _('Th'), _('Fr'), _('Sa')] 156 datepicker_options[:day_names_min] ||= [_('Su'), _('Mo'), _('Tu'), _('We'), _('Th'), _('Fr'), _('Sa')]
157 datepicker_options[:day_names_short] ||= [_('Sun'), _('Mon'), _('Tue'), _('Wed'), _('Thu'), _('Fri'), _('Sat')] 157 datepicker_options[:day_names_short] ||= [_('Sun'), _('Mon'), _('Tue'), _('Wed'), _('Thu'), _('Fri'), _('Sat')]
@@ -236,7 +236,7 @@ module FormsHelper @@ -236,7 +236,7 @@ module FormsHelper
236 weekHeader: #{datepicker_options[:week_header].to_json}, 236 weekHeader: #{datepicker_options[:week_header].to_json},
237 yearRange: #{datepicker_options[:year_range].to_json}, 237 yearRange: #{datepicker_options[:year_range].to_json},
238 yearSuffix: #{datepicker_options[:year_suffix].to_json} 238 yearSuffix: #{datepicker_options[:year_suffix].to_json}
239 - }) 239 + }).datepicker('setDate', new Date('#{value}'))
240 </script> 240 </script>
241 ".html_safe 241 ".html_safe
242 result 242 result
app/helpers/layout_helper.rb
@@ -86,7 +86,7 @@ module LayoutHelper @@ -86,7 +86,7 @@ module LayoutHelper
86 end 86 end
87 87
88 def theme_stylesheet_path 88 def theme_stylesheet_path
89 - "/assets#{theme_path}/style.css" 89 + "#{theme_path}/style.css".gsub(%r{^/}, '')
90 end 90 end
91 91
92 def layout_template 92 def layout_template
app/models/article.rb
@@ -9,7 +9,7 @@ class Article &lt; ActiveRecord::Base @@ -9,7 +9,7 @@ class Article &lt; ActiveRecord::Base
9 :highlighted, :notify_comments, :display_hits, :slug, 9 :highlighted, :notify_comments, :display_hits, :slug,
10 :external_feed_builder, :display_versions, :external_link, 10 :external_feed_builder, :display_versions, :external_link,
11 :image_builder, :show_to_followers, 11 :image_builder, :show_to_followers,
12 - :author 12 + :author, :display_preview
13 13
14 acts_as_having_image 14 acts_as_having_image
15 15
@@ -637,6 +637,20 @@ class Article &lt; ActiveRecord::Base @@ -637,6 +637,20 @@ class Article &lt; ActiveRecord::Base
637 can_display_hits? && display_hits 637 can_display_hits? && display_hits
638 end 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 def image? 654 def image?
641 false 655 false
642 end 656 end
@@ -745,9 +759,10 @@ class Article &lt; ActiveRecord::Base @@ -745,9 +759,10 @@ class Article &lt; ActiveRecord::Base
745 end 759 end
746 760
747 def body_images_paths 761 def body_images_paths
748 - require 'uri'  
749 Nokogiri::HTML.fragment(self.body.to_s).css('img[src]').collect do |i| 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 end 766 end
752 end 767 end
753 768
app/models/category.rb
@@ -81,7 +81,7 @@ class Category &lt; ActiveRecord::Base @@ -81,7 +81,7 @@ class Category &lt; ActiveRecord::Base
81 end 81 end
82 82
83 def upcoming_events(limit = 10) 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 end 85 end
86 86
87 def display_in_menu? 87 def display_in_menu?
app/models/enterprise_homepage.rb
@@ -35,4 +35,8 @@ class EnterpriseHomepage &lt; Article @@ -35,4 +35,8 @@ class EnterpriseHomepage &lt; Article
35 false 35 false
36 end 36 end
37 37
  38 + def can_display_media_panel?
  39 + true
  40 + end
  41 +
38 end 42 end
app/models/environment.rb
@@ -13,7 +13,7 @@ class Environment &lt; ActiveRecord::Base @@ -13,7 +13,7 @@ class Environment &lt; ActiveRecord::Base
13 :reports_lower_bound, :noreply_email, 13 :reports_lower_bound, :noreply_email,
14 :signup_welcome_screen_body, :members_whitelist_enabled, 14 :signup_welcome_screen_body, :members_whitelist_enabled,
15 :members_whitelist, :highlighted_news_amount, 15 :members_whitelist, :highlighted_news_amount,
16 - :portal_news_amount, :date_format 16 + :portal_news_amount, :date_format, :signup_intro
17 17
18 has_many :users 18 has_many :users
19 19
app/models/event.rb
@@ -23,7 +23,7 @@ class Event &lt; Article @@ -23,7 +23,7 @@ class Event &lt; Article
23 23
24 def initialize(*args) 24 def initialize(*args)
25 super(*args) 25 super(*args)
26 - self.start_date ||= Date.today 26 + self.start_date ||= DateTime.now
27 end 27 end
28 28
29 validates_presence_of :title, :start_date 29 validates_presence_of :title, :start_date
@@ -35,8 +35,9 @@ class Event &lt; Article @@ -35,8 +35,9 @@ class Event &lt; Article
35 end 35 end
36 36
37 scope :by_day, -> (date) { 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 order('start_date ASC') 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 scope :next_events_from_month, -> (date) { 43 scope :next_events_from_month, -> (date) {
@@ -75,7 +76,7 @@ class Event &lt; Article @@ -75,7 +76,7 @@ class Event &lt; Article
75 76
76 def self.date_range(year, month) 77 def self.date_range(year, month)
77 if year.nil? || month.nil? 78 if year.nil? || month.nil?
78 - today = Date.today 79 + today = DateTime.now
79 year = today.year 80 year = today.year
80 month = today.month 81 month = today.month
81 else 82 else
@@ -83,7 +84,7 @@ class Event &lt; Article @@ -83,7 +84,7 @@ class Event &lt; Article
83 month = month.to_i 84 month = month.to_i
84 end 85 end
85 86
86 - first_day = Date.new(year, month, 1) 87 + first_day = DateTime.new(year, month, 1)
87 last_day = first_day + 1.month - 1.day 88 last_day = first_day + 1.month - 1.day
88 89
89 first_day..last_day 90 first_day..last_day
@@ -109,7 +110,7 @@ class Event &lt; Article @@ -109,7 +110,7 @@ class Event &lt; Article
109 end 110 end
110 111
111 def duration 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 end 114 end
114 115
115 alias_method :article_lead, :lead 116 alias_method :article_lead, :lead
@@ -129,6 +130,10 @@ class Event &lt; Article @@ -129,6 +130,10 @@ class Event &lt; Article
129 true 130 true
130 end 131 end
131 132
  133 + def can_display_media_panel?
  134 + true
  135 + end
  136 +
132 include Noosfero::TranslatableContent 137 include Noosfero::TranslatableContent
133 include MaybeAddHttp 138 include MaybeAddHttp
134 139
app/models/profile.rb
@@ -579,6 +579,14 @@ class Profile &lt; ActiveRecord::Base @@ -579,6 +579,14 @@ class Profile &lt; ActiveRecord::Base
579 options.merge(Noosfero.url_options) 579 options.merge(Noosfero.url_options)
580 end 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 private :generate_url, :url_options 590 private :generate_url, :url_options
583 591
584 def default_hostname 592 def default_hostname
app/models/text_article.rb
@@ -33,12 +33,16 @@ class TextArticle &lt; Article @@ -33,12 +33,16 @@ class TextArticle &lt; Article
33 end 33 end
34 34
35 def change_element_path(el, attribute) 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 if fullpath 37 if fullpath
38 domain = fullpath[2] 38 domain = fullpath[2]
39 path = fullpath[4] 39 path = fullpath[4]
40 - el[attribute] = path if domain == environment.default_hostname 40 + el[attribute] = path if domain == profile.default_hostname
41 end 41 end
42 end 42 end
43 43
  44 + def display_preview?
  45 + parent && parent.kind_of?(Blog) && parent.display_preview
  46 + end
  47 +
44 end 48 end
app/models/textile_article.rb
@@ -24,6 +24,10 @@ class TextileArticle &lt; TextArticle @@ -24,6 +24,10 @@ class TextileArticle &lt; TextArticle
24 true 24 true
25 end 25 end
26 26
  27 + def can_display_media_panel?
  28 + true
  29 + end
  30 +
27 protected 31 protected
28 32
29 def convert_to_html(textile) 33 def convert_to_html(textile)
app/models/tiny_mce_article.rb
@@ -28,4 +28,8 @@ class TinyMceArticle &lt; TextArticle @@ -28,4 +28,8 @@ class TinyMceArticle &lt; TextArticle
28 true 28 true
29 end 29 end
30 30
  31 + def can_display_media_panel?
  32 + true
  33 + end
  34 +
31 end 35 end
app/models/user.rb
@@ -121,11 +121,17 @@ class User &lt; ActiveRecord::Base @@ -121,11 +121,17 @@ class User &lt; ActiveRecord::Base
121 121
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 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 # Authenticates a user by their login name or email and unencrypted password. Returns the user or nil. 129 # Authenticates a user by their login name or email and unencrypted password. Returns the user or nil.
125 def self.authenticate(login, password, environment = nil) 130 def self.authenticate(login, password, environment = nil)
126 environment ||= Environment.default 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 u && u.authenticated?(password) ? u : nil 135 u && u.authenticated?(password) ? u : nil
130 end 136 end
131 137
@@ -237,7 +243,23 @@ class User &lt; ActiveRecord::Base @@ -237,7 +243,23 @@ class User &lt; ActiveRecord::Base
237 password.crypt(salt) 243 password.crypt(salt)
238 end 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 def authenticated?(password) 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 result = (crypted_password == encrypt(password)) 263 result = (crypted_password == encrypt(password))
242 if (encryption_method != User.system_encryption_method) && result 264 if (encryption_method != User.system_encryption_method) && result
243 self.password_type = User.system_encryption_method.to_s 265 self.password_type = User.system_encryption_method.to_s
@@ -276,9 +298,15 @@ class User &lt; ActiveRecord::Base @@ -276,9 +298,15 @@ class User &lt; ActiveRecord::Base
276 # current password. 298 # current password.
277 # * Saves the record unless it is a new one. 299 # * Saves the record unless it is a new one.
278 def change_password!(current, new, confirmation) 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 end 310 end
283 self.force_change_password!(new, confirmation) 311 self.force_change_password!(new, confirmation)
284 end 312 end
app/views/blocks/profile_info_actions/_join_leave_community.html.erb
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 class: 'join-community', 11 class: 'join-community',
12 style: 'position: relative; display: none;' %> 12 style: 'position: relative; display: none;' %>
13 <% else %> 13 <% else %>
14 - <%= button :add, _('Join this community'), profile.join_url %> 14 + <%= button :add, _('Join this community'), profile.join_url, class: 'join-community' %>
15 <% end %> 15 <% end %>
16 <% end %> 16 <% end %>
17 <% else %> 17 <% else %>
app/views/cms/_event.html.erb
@@ -8,9 +8,8 @@ @@ -8,9 +8,8 @@
8 <%= render :partial => 'general_fields' %> 8 <%= render :partial => 'general_fields' %>
9 <%= render :partial => 'translatable' %> 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 <%= labelled_form_field(_('Event website:'), text_field(:article, :link)) %> 14 <%= labelled_form_field(_('Event website:'), text_field(:article, :link)) %>
16 15
app/views/cms/edit.html.erb
1 <%= error_messages_for 'article' %> 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 <%= labelled_form_for 'article', :html => { :multipart => true, :class => @type } do |f| %> 4 <%= labelled_form_for 'article', :html => { :multipart => true, :class => @type } do |f| %>
7 5
8 <%= hidden_field_tag("type", @type) if @type %> 6 <%= hidden_field_tag("type", @type) if @type %>
@@ -68,7 +66,7 @@ @@ -68,7 +66,7 @@
68 <% end %> 66 <% end %>
69 </div> 67 </div>
70 68
71 -<% if show_media_panel %> 69 +<% if @article.display_media_panel? %>
72 <%= render :partial => 'text_editor_sidebar' %> 70 <%= render :partial => 'text_editor_sidebar' %>
73 <% end %> 71 <% end %>
74 72
app/views/content_viewer/_article_title.html.erb
@@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
7 <% end %> 7 <% end %>
8 </h1> 8 </h1>
9 <%= render :partial => "publishing_info" %> 9 <%= render :partial => "publishing_info" %>
10 - <% unless @page.abstract.blank? %> 10 + <% if @page.display_preview? %>
11 <div class="preview"> 11 <div class="preview">
12 <%= @page.lead %> 12 <%= @page.lead %>
13 </div> 13 </div>
app/views/content_viewer/_publishing_info.html.erb
1 <span class="publishing-info"> 1 <span class="publishing-info">
2 <span class="date"> 2 <span class="date">
3 - <%= show_date(@page.published_at) %> 3 + <%= show_time(@page.published_at) %>
4 </span> 4 </span>
5 <span class="author"> 5 <span class="author">
6 <%= _(", by %s") % (@page.author ? link_to(@page.author_name, @page.author_url) : @page.author_name) %> 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,7 +2,7 @@
2 <%= link_to '', 2 <%= link_to '',
3 uploaded_file.view_url, 3 uploaded_file.view_url,
4 :class => 'image', 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 <span><%=h uploaded_file.title %></span> 7 <span><%=h uploaded_file.title %></span>
8 <% else %> 8 <% else %>
db/migrate/20150722042714_change_article_date_to_datetime.rb 0 → 100644
@@ -0,0 +1,27 @@ @@ -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
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 # 11 #
12 # It's strongly recommended to check this file into your version control system. 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 create_table "abuse_reports", :force => true do |t| 16 create_table "abuse_reports", :force => true do |t|
17 t.integer "reporter_id" 17 t.integer "reporter_id"
@@ -75,8 +75,8 @@ ActiveRecord::Schema.define(:version =&gt; 20150712130827) do @@ -75,8 +75,8 @@ ActiveRecord::Schema.define(:version =&gt; 20150712130827) do
75 t.integer "comments_count" 75 t.integer "comments_count"
76 t.boolean "advertise", :default => true 76 t.boolean "advertise", :default => true
77 t.boolean "published", :default => true 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 t.integer "children_count", :default => 0 80 t.integer "children_count", :default => 0
81 t.boolean "accept_comments", :default => true 81 t.boolean "accept_comments", :default => true
82 t.integer "reference_article_id" 82 t.integer "reference_article_id"
@@ -127,8 +127,8 @@ ActiveRecord::Schema.define(:version =&gt; 20150712130827) do @@ -127,8 +127,8 @@ ActiveRecord::Schema.define(:version =&gt; 20150712130827) do
127 t.integer "comments_count", :default => 0 127 t.integer "comments_count", :default => 0
128 t.boolean "advertise", :default => true 128 t.boolean "advertise", :default => true
129 t.boolean "published", :default => true 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 t.integer "children_count", :default => 0 132 t.integer "children_count", :default => 0
133 t.boolean "accept_comments", :default => true 133 t.boolean "accept_comments", :default => true
134 t.integer "reference_article_id" 134 t.integer "reference_article_id"
features/events.feature
@@ -223,7 +223,7 @@ Feature: events @@ -223,7 +223,7 @@ Feature: events
223 | owner | name | start_date | end_date | 223 | owner | name | start_date | end_date |
224 | josesilva | WikiSym 2009 | 2009-10-25 | 2009-10-27 | 224 | josesilva | WikiSym 2009 | 2009-10-25 | 2009-10-27 |
225 When I am on /profile/josesilva/events/2009/10/26 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 Scenario: show place of the event 228 Scenario: show place of the event
229 Given I am on /profile/josesilva/events/2009/10 229 Given I am on /profile/josesilva/events/2009/10
lib/tasks/release.rake
@@ -239,12 +239,12 @@ EOF @@ -239,12 +239,12 @@ EOF
239 end 239 end
240 end 240 end
241 241
242 - Rake::Task['noosfero:upload'].invoke  
243 if confirm('Upload the packages') 242 if confirm('Upload the packages')
244 puts "==> Uploading debian packages..." 243 puts "==> Uploading debian packages..."
245 Rake::Task['noosfero:upload_packages'].invoke(target) 244 Rake::Task['noosfero:upload_packages'].invoke(target)
246 else 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 end 248 end
249 249
250 rm_f "tmp/pending-release" 250 rm_f "tmp/pending-release"
plugins/analytics/controllers/myprofile/analytics_plugin/stats_controller.rb 0 → 100644
@@ -0,0 +1,21 @@ @@ -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 module AnalyticsPlugin 1 module AnalyticsPlugin
2 2
3 - TimeOnPageUpdateInterval = 2.minutes * 1000 3 + TimeOnPageUpdateInterval = 2.minutes
  4 + TimeOnPageUpdateIntervalMs = TimeOnPageUpdateInterval * 1000
4 5
5 extend Noosfero::Plugin::ParentMethods 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,4 +38,12 @@ class AnalyticsPlugin::Base &lt; Noosfero::Plugin
38 }] 38 }]
39 end 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 end 49 end
plugins/analytics/lib/ext/profile.rb
@@ -13,7 +13,7 @@ end @@ -13,7 +13,7 @@ end
13 class Profile 13 class Profile
14 14
15 def analytics_settings attrs = {} 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 attrs.each{ |a, v| @analytics_settings.send "#{a}=", v } 17 attrs.each{ |a, v| @analytics_settings.send "#{a}=", v }
18 @analytics_settings 18 @analytics_settings
19 end 19 end
plugins/analytics/locales/en.yml
@@ -5,6 +5,13 @@ en: &amp;en @@ -5,6 +5,13 @@ en: &amp;en
5 plugin: 5 plugin:
6 name: 'Access tracking' 6 name: 'Access tracking'
7 description: 'Register the access of selected profiles' 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 en-US: 16 en-US:
10 <<: *en 17 <<: *en
plugins/analytics/locales/pt.yml
@@ -5,6 +5,13 @@ pt: &amp;pt @@ -5,6 +5,13 @@ pt: &amp;pt
5 plugin: 5 plugin:
6 name: 'Rastreio de accesso' 6 name: 'Rastreio de accesso'
7 description: 'Registra o acesso de perfis selecionados' 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 pt-BR: 16 pt-BR:
10 <<: *pt 17 <<: *pt
plugins/analytics/models/analytics_plugin/page_view.rb
@@ -25,10 +25,24 @@ class AnalyticsPlugin::PageView &lt; ActiveRecord::Base @@ -25,10 +25,24 @@ class AnalyticsPlugin::PageView &lt; ActiveRecord::Base
25 before_validation :fill_referer_page_view, on: :create 25 before_validation :fill_referer_page_view, on: :create
26 before_validation :fill_visit, on: :create 26 before_validation :fill_visit, on: :create
27 27
  28 + scope :latest, -> { order 'request_started_at DESC' }
  29 +
28 def request_duration 30 def request_duration
29 self.request_finished_at - self.request_started_at 31 self.request_finished_at - self.request_started_at
30 end 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 def page_load! 46 def page_load!
33 self.page_loaded_at = Time.now 47 self.page_loaded_at = Time.now
34 self.update_column :page_loaded_at, self.page_loaded_at 48 self.update_column :page_loaded_at, self.page_loaded_at
@@ -36,10 +50,9 @@ class AnalyticsPlugin::PageView &lt; ActiveRecord::Base @@ -36,10 +50,9 @@ class AnalyticsPlugin::PageView &lt; ActiveRecord::Base
36 50
37 def increase_time_on_page! 51 def increase_time_on_page!
38 now = Time.now 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 self.update_column :time_on_page, self.time_on_page 56 self.update_column :time_on_page, self.time_on_page
44 end 57 end
45 58
@@ -59,7 +72,7 @@ class AnalyticsPlugin::PageView &lt; ActiveRecord::Base @@ -59,7 +72,7 @@ class AnalyticsPlugin::PageView &lt; ActiveRecord::Base
59 end 72 end
60 73
61 def fill_visit 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 self.visit ||= AnalyticsPlugin::Visit.new profile: profile 76 self.visit ||= AnalyticsPlugin::Visit.new profile: profile
64 end 77 end
65 78
plugins/analytics/models/analytics_plugin/visit.rb
@@ -3,9 +3,17 @@ class AnalyticsPlugin::Visit &lt; ActiveRecord::Base @@ -3,9 +3,17 @@ class AnalyticsPlugin::Visit &lt; ActiveRecord::Base
3 attr_accessible *self.column_names 3 attr_accessible *self.column_names
4 attr_accessible :profile 4 attr_accessible :profile
5 5
6 - default_scope -> { includes :page_views }  
7 -  
8 belongs_to :profile 6 belongs_to :profile
9 has_many :page_views, class_name: 'AnalyticsPlugin::PageView', dependent: :destroy 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 end 19 end
plugins/analytics/test/functional/content_viewer_controller_test.rb
@@ -31,6 +31,8 @@ class ContentViewerControllerTest &lt; ActionController::TestCase @@ -31,6 +31,8 @@ class ContentViewerControllerTest &lt; ActionController::TestCase
31 31
32 first_page_view = @community.page_views.order(:id).first 32 first_page_view = @community.page_views.order(:id).first
33 assert_equal @request.referer, first_page_view.referer_url 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 @request.env['HTTP_REFERER'] = first_url 37 @request.env['HTTP_REFERER'] = first_url
36 get :view_page, profile: @community.identifier, page: @community.articles.last.path.split('/') 38 get :view_page, profile: @community.identifier, page: @community.articles.last.path.split('/')
@@ -40,9 +42,13 @@ class ContentViewerControllerTest &lt; ActionController::TestCase @@ -40,9 +42,13 @@ class ContentViewerControllerTest &lt; ActionController::TestCase
40 second_page_view = @community.page_views.order(:id).last 42 second_page_view = @community.page_views.order(:id).last
41 assert_equal first_page_view, second_page_view.referer_page_view 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 end 52 end
47 53
48 end 54 end
plugins/analytics/views/analytics_plugin/_body_ending.html.slim
1 javascript: 1 javascript:
2 analytics.timeOnPage.baseUrl = #{url_for(controller: 'analytics_plugin/time_on_page').to_json} 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 analytics.requestId = #{request.env['action_dispatch.request_id'].to_json} 4 analytics.requestId = #{request.env['action_dispatch.request_id'].to_json}
5 analytics.init() 5 analytics.init()
6 6
plugins/analytics/views/analytics_plugin/stats/_table.html.slim 0 → 100644
@@ -0,0 +1,38 @@ @@ -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 @@ @@ -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,8 +29,8 @@ class CommunityTrackPlugin::Step &lt; Folder
29 29
30 def initialize(*args) 30 def initialize(*args)
31 super(*args) 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 end 34 end
35 35
36 def set_hidden_position 36 def set_hidden_position
@@ -72,20 +72,20 @@ class CommunityTrackPlugin::Step &lt; Folder @@ -72,20 +72,20 @@ class CommunityTrackPlugin::Step &lt; Folder
72 end 72 end
73 73
74 def active? 74 def active?
75 - (start_date..end_date).include?(Date.today) 75 + (start_date..end_date).cover?(DateTime.now)
76 end 76 end
77 77
78 def finished? 78 def finished?
79 - Date.today > end_date 79 + DateTime.now > end_date
80 end 80 end
81 81
82 def waiting? 82 def waiting?
83 - Date.today < start_date 83 + DateTime.now < start_date
84 end 84 end
85 85
86 def schedule_activation 86 def schedule_activation
87 return if !changes['start_date'] && !changes['end_date'] 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 schedule_date = !accept_comments ? start_date : end_date + 1.day 89 schedule_date = !accept_comments ? start_date : end_date + 1.day
90 CommunityTrackPlugin::ActivationJob.find(id).destroy_all 90 CommunityTrackPlugin::ActivationJob.find(id).destroy_all
91 Delayed::Job.enqueue(CommunityTrackPlugin::ActivationJob.new(self.id), :run_at => schedule_date) 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,7 +5,7 @@ class ContentViewerControllerTest &lt; ActionController::TestCase
5 def setup 5 def setup
6 @profile = Community.create!(:name => 'Sample community', :identifier => 'sample-community') 6 @profile = Community.create!(:name => 'Sample community', :identifier => 'sample-community')
7 @track = create_track('track', @profile) 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 user = create_user('testinguser') 10 user = create_user('testinguser')
11 login_as(user.login) 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,7 +9,7 @@ class StepTest &lt; ActiveSupport::TestCase
9 @track.add_category(@category) 9 @track.add_category(@category)
10 @track.save! 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 Delayed::Job.destroy_all 13 Delayed::Job.destroy_all
14 end 14 end
15 15
@@ -22,39 +22,39 @@ class StepTest &lt; ActiveSupport::TestCase @@ -22,39 +22,39 @@ class StepTest &lt; ActiveSupport::TestCase
22 end 22 end
23 23
24 should 'set accept_comments to false on create' do 24 should 'set accept_comments to false on create' do
25 - today = Date.today 25 + today = DateTime.now
26 step = CommunityTrackPlugin::Step.create(:name => 'Step', :body => 'body', :profile => @profile, :parent => @track, :start_date => today, :end_date => today, :published => true) 26 step = CommunityTrackPlugin::Step.create(:name => 'Step', :body => 'body', :profile => @profile, :parent => @track, :start_date => today, :end_date => today, :published => true)
27 refute step.accept_comments 27 refute step.accept_comments
28 end 28 end
29 29
30 should 'do not allow step creation with a parent that is not a track' do 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 blog = fast_create(Blog) 32 blog = fast_create(Blog)
33 step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => blog, :start_date => today, :end_date => today, :published => true) 33 step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => blog, :start_date => today, :end_date => today, :published => true)
34 refute step.save 34 refute step.save
35 end 35 end
36 36
37 should 'do not allow step creation without a parent' do 37 should 'do not allow step creation without a parent' do
38 - today = Date.today 38 + today = DateTime.now
39 step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => nil, :start_date => today, :end_date => today, :published => true) 39 step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => nil, :start_date => today, :end_date => today, :published => true)
40 refute step.save 40 refute step.save
41 end 41 end
42 42
43 should 'create step if end date is equal to start date' do 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 assert @step.save 46 assert @step.save
47 end 47 end
48 48
49 should 'create step if end date is after start date' do 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 assert @step.save 52 assert @step.save
53 end 53 end
54 54
55 should 'do not create step if end date is before start date' do 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 refute @step.save 58 refute @step.save
59 end 59 end
60 60
@@ -71,20 +71,20 @@ class StepTest &lt; ActiveSupport::TestCase @@ -71,20 +71,20 @@ class StepTest &lt; ActiveSupport::TestCase
71 end 71 end
72 72
73 should 'be active if today is between start and end dates' do 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 assert @step.active? 76 assert @step.active?
77 end 77 end
78 78
79 should 'be finished if today is after the end date' do 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 assert @step.finished? 82 assert @step.finished?
83 end 83 end
84 84
85 should 'be waiting if today is before the end date' do 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 assert @step.waiting? 88 assert @step.waiting?
89 end 89 end
90 90
@@ -95,17 +95,17 @@ class StepTest &lt; ActiveSupport::TestCase @@ -95,17 +95,17 @@ class StepTest &lt; ActiveSupport::TestCase
95 end 95 end
96 96
97 should 'create delayed job' do 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 @step.accept_comments = false 100 @step.accept_comments = false
101 @step.schedule_activation 101 @step.schedule_activation
102 assert_equal 1, Delayed::Job.count 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 end 104 end
105 105
106 should 'do not duplicate delayed job' do 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 @step.schedule_activation 109 @step.schedule_activation
110 assert_equal 1, Delayed::Job.count 110 assert_equal 1, Delayed::Job.count
111 @step.schedule_activation 111 @step.schedule_activation
@@ -113,30 +113,30 @@ class StepTest &lt; ActiveSupport::TestCase @@ -113,30 +113,30 @@ class StepTest &lt; ActiveSupport::TestCase
113 end 113 end
114 114
115 should 'create delayed job when a step is saved' do 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 @step.save! 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 end 120 end
121 121
122 should 'create delayed job even if start date has passed' do 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 @step.accept_comments = false 125 @step.accept_comments = false
126 @step.schedule_activation 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 end 128 end
129 129
130 should 'create delayed job if end date has passed' do 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 @step.schedule_activation 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 end 135 end
136 136
137 should 'do not schedule delayed job if save but do not modify date fields' do 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 @step.save! 140 @step.save!
141 assert_equal 1, Delayed::Job.count 141 assert_equal 1, Delayed::Job.count
142 Delayed::Job.destroy_all 142 Delayed::Job.destroy_all
@@ -149,13 +149,13 @@ class StepTest &lt; ActiveSupport::TestCase @@ -149,13 +149,13 @@ class StepTest &lt; ActiveSupport::TestCase
149 refute @step.position 149 refute @step.position
150 @step.save! 150 @step.save!
151 assert_equal 1, @step.position 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 step2.save! 153 step2.save!
154 assert_equal 2, step2.position 154 assert_equal 2, step2.position
155 end 155 end
156 156
157 should 'accept comments if step is active' do 157 should 'accept comments if step is active' do
158 - @step.start_date = Date.today 158 + @step.start_date = DateTime.now
159 @step.save! 159 @step.save!
160 refute @step.accept_comments 160 refute @step.accept_comments
161 @step.toggle_activation 161 @step.toggle_activation
@@ -164,8 +164,8 @@ class StepTest &lt; ActiveSupport::TestCase @@ -164,8 +164,8 @@ class StepTest &lt; ActiveSupport::TestCase
164 end 164 end
165 165
166 should 'do not accept comments if step is not active' do 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 @step.save! 169 @step.save!
170 refute @step.published 170 refute @step.published
171 @step.toggle_activation 171 @step.toggle_activation
@@ -174,14 +174,14 @@ class StepTest &lt; ActiveSupport::TestCase @@ -174,14 +174,14 @@ class StepTest &lt; ActiveSupport::TestCase
174 end 174 end
175 175
176 should 'do not accept comments if step is not active anymore' do 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 @step.save! 178 @step.save!
179 @step.toggle_activation 179 @step.toggle_activation
180 @step.reload 180 @step.reload
181 assert @step.accept_comments 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 @step.save! 185 @step.save!
186 @step.toggle_activation 186 @step.toggle_activation
187 @step.reload 187 @step.reload
@@ -203,7 +203,7 @@ class StepTest &lt; ActiveSupport::TestCase @@ -203,7 +203,7 @@ class StepTest &lt; ActiveSupport::TestCase
203 end 203 end
204 204
205 should 'change position to botton if a hidden step becomes visible' do 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 step1.save! 207 step1.save!
208 @step.hidden = true 208 @step.hidden = true
209 @step.save! 209 @step.save!
@@ -215,7 +215,7 @@ class StepTest &lt; ActiveSupport::TestCase @@ -215,7 +215,7 @@ class StepTest &lt; ActiveSupport::TestCase
215 215
216 should 'decrement lower items positions if a step becomes hidden' do 216 should 'decrement lower items positions if a step becomes hidden' do
217 @step.save! 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 step1.save! 219 step1.save!
220 assert_equal 2, step1.position 220 assert_equal 2, step1.position
221 @step.hidden = true 221 @step.hidden = true
@@ -225,7 +225,7 @@ class StepTest &lt; ActiveSupport::TestCase @@ -225,7 +225,7 @@ class StepTest &lt; ActiveSupport::TestCase
225 end 225 end
226 226
227 should 'do not publish a hidden step' do 227 should 'do not publish a hidden step' do
228 - @step.start_date = Date.today 228 + @step.start_date = DateTime.now
229 @step.hidden = true 229 @step.hidden = true
230 @step.save! 230 @step.save!
231 refute @step.published 231 refute @step.published
@@ -266,7 +266,7 @@ class StepTest &lt; ActiveSupport::TestCase @@ -266,7 +266,7 @@ class StepTest &lt; ActiveSupport::TestCase
266 end 266 end
267 267
268 should 'enable comments on children when step is activated' do 268 should 'enable comments on children when step is activated' do
269 - @step.start_date = Date.today 269 + @step.start_date = DateTime.now
270 @step.save! 270 @step.save!
271 refute @step.accept_comments 271 refute @step.accept_comments
272 article = fast_create(Article, :parent_id => @step.id, :profile_id => @step.profile.id, :accept_comments => false) 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,8 +276,7 @@ class StepTest &lt; ActiveSupport::TestCase
276 end 276 end
277 277
278 should 'enable comments on children when step is active' do 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 @step.save! 280 @step.save!
282 refute @step.accept_comments 281 refute @step.accept_comments
283 @step.toggle_activation 282 @step.toggle_activation
plugins/event/lib/event_plugin/event_block.rb
@@ -30,13 +30,13 @@ class EventPlugin::EventBlock &lt; Block @@ -30,13 +30,13 @@ class EventPlugin::EventBlock &lt; Block
30 events = user.nil? ? events.is_public : events.display_filter(user,nil) 30 events = user.nil? ? events.is_public : events.display_filter(user,nil)
31 31
32 if future_only 32 if future_only
33 - events = events.where('start_date >= ?', Date.today) 33 + events = events.where('start_date >= ?', DateTime.now.beginning_of_day)
34 end 34 end
35 35
36 if date_distance_limit > 0 36 if date_distance_limit > 0
37 events = events.by_range([ 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 end 41 end
42 42
plugins/event/test/functional/event_block_test.rb
@@ -7,7 +7,7 @@ class HomeControllerTest &lt; ActionController::TestCase @@ -7,7 +7,7 @@ class HomeControllerTest &lt; ActionController::TestCase
7 @env.enable_plugin('EventPlugin') 7 @env.enable_plugin('EventPlugin')
8 8
9 @p1 = fast_create(Person, :environment_id => @env.id) 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 box = Box.create!(:owner => @env) 12 box = Box.create!(:owner => @env)
13 @block = EventPlugin::EventBlock.create!(:box => box) 13 @block = EventPlugin::EventBlock.create!(:box => box)
@@ -19,6 +19,7 @@ class HomeControllerTest &lt; ActionController::TestCase @@ -19,6 +19,7 @@ class HomeControllerTest &lt; ActionController::TestCase
19 19
20 should 'see events microdata sturcture' do 20 should 'see events microdata sturcture' do
21 get :index 21 get :index
  22 +#raise response.body.inspect
22 assert_select '.event-plugin_event-block ul.events' 23 assert_select '.event-plugin_event-block ul.events'
23 assert_select ev 24 assert_select ev
24 assert_select ev + 'a[itemprop="url"]' 25 assert_select ev + 'a[itemprop="url"]'
@@ -33,15 +34,15 @@ class HomeControllerTest &lt; ActionController::TestCase @@ -33,15 +34,15 @@ class HomeControllerTest &lt; ActionController::TestCase
33 34
34 should 'see event duration' do 35 should 'see event duration' do
35 @e1a.slug = 'event1a' 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 @e1a.save! 39 @e1a.save!
39 get :index 40 get :index
40 assert_select ev + 'time.duration[itemprop="endDate"]', /1 day/ 41 assert_select ev + 'time.duration[itemprop="endDate"]', /1 day/
41 42
42 @e1a.slug = 'event1a' 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 @e1a.save! 46 @e1a.save!
46 get :index 47 get :index
47 assert_select ev + 'time.duration[itemprop="endDate"]', /2 days/ 48 assert_select ev + 'time.duration[itemprop="endDate"]', /2 days/
@@ -52,8 +53,8 @@ class HomeControllerTest &lt; ActionController::TestCase @@ -52,8 +53,8 @@ class HomeControllerTest &lt; ActionController::TestCase
52 assert_select ev + 'time.duration[itemprop="endDate"]', false 53 assert_select ev + 'time.duration[itemprop="endDate"]', false
53 54
54 @e1a.slug = 'event1a' 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 @e1a.save! 58 @e1a.save!
58 get :index 59 get :index
59 assert_select ev + 'time.duration[itemprop="endDate"]', false 60 assert_select ev + 'time.duration[itemprop="endDate"]', false
plugins/event/views/blocks/event.html.erb
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 2
3 <ul class="events"> 3 <ul class="events">
4 <% block.events(user).map do |event| %> 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 <li itemscope="itemscope" itemtype="http://data-vocabulary.org/Event" class="event"> 6 <li itemscope="itemscope" itemtype="http://data-vocabulary.org/Event" class="event">
7 <%= render( 7 <%= render(
8 :file => 'event_plugin/event_block_item', 8 :file => 'event_plugin/event_block_item',
plugins/fb_app/Gemfile 0 → 100644
@@ -0,0 +1,10 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -0,0 +1 @@
  1 +FB-f-Logo__blue_48.png
0 \ No newline at end of file 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 @@ @@ -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 @@ @@ -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 @@ @@ -0,0 +1 @@
  1 +stylesheets/style.scss
0 \ No newline at end of file 2 \ No newline at end of file
plugins/fb_app/public/stylesheets/_base.scss 0 → 100644
@@ -0,0 +1,214 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -0,0 +1,21 @@
  1 +#fb-app-catalog-wrapper class=('fb-app-standalone' if @signed_request.blank?)
  2 +
  3 + = render 'load'
  4 + = render 'title_and_subtitle'
  5 +
  6 + #product-catalog
  7 + #product-catalog-actions
  8 + - if @page_tab.config_type == :profile
  9 + - if profile and user.present? and (user.is_admin?(environment) or user.is_admin?(profile))
  10 + .product-catalog-ctrl
  11 + = button :add, _('Add product or service'), controller: :manage_products, action: :new, profile: profile.identifier
  12 + = render 'configure_button'
  13 + = content_for :product_page do
  14 + = render 'catalog/results'
  15 + = render 'catalog/search'
  16 + = render 'catalog/javascripts', external: false
  17 +
  18 + = render 'footer'
  19 +
  20 + javascript:
  21 + catalog.base_url_path = #{url_for(controller: :fb_app_plugin_page_tab, action: :index, page_id: params[:page_id], signed_request: params[:signed_request]).to_json} + '&'
plugins/fb_app/views/fb_app_plugin_page_tab/first_load.html.slim 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +javascript:
  2 + noosfero.modal.url(#{url_for(controller: :fb_app_plugin, action: :admin, page_id: @page_ids).to_json})
  3 +
plugins/fb_app/views/fb_app_plugin_page_tab/product.html.slim 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 += render 'title_and_subtitle'
  2 +
  3 += button :back, t('fb_app_plugin.views.page_tab.back_to_catalog'), params.except(:product_id), target: ''
  4 +
  5 += render file: "#{Rails.root}/app/views/manage_products/show.html.erb"
  6 +
  7 += button :back, t('fb_app_plugin.views.page_tab.back_to_catalog'), params.except(:product_id), target: ''
  8 +
  9 += render 'footer'
  10 +
  11 += render 'load'
plugins/metadata/lib/ext/article.rb
@@ -12,9 +12,9 @@ class Article @@ -12,9 +12,9 @@ class Article
12 end, 12 end,
13 title: proc{ |a, plugin| "#{a.title} - #{a.profile.name}" }, 13 title: proc{ |a, plugin| "#{a.title} - #{a.profile.name}" },
14 image: proc do |a, plugin| 14 image: proc do |a, plugin|
15 - img = a.body_images_paths  
16 - img = "#{a.profile.environment.top_url}#{a.profile.image.public_filename}" if a.profile.image if img.blank?  
17 - img ||= MetadataPlugin.config[:open_graph][:environment_logo] rescue nil if img.blank? 15 + img = a.body_images_paths.map! &:html_safe
  16 + img = "#{a.profile.environment.top_url}#{a.profile.image.public_filename}".html_safe if a.profile.image if img.blank?
  17 + img ||= MetadataPlugin.config[:open_graph][:environment_logo].html_safe rescue nil if img.blank?
18 img 18 img
19 end, 19 end,
20 see_also: [], 20 see_also: [],
@@ -31,10 +31,10 @@ class Article @@ -31,10 +31,10 @@ class Article
31 card: 'summary', 31 card: 'summary',
32 description: proc do |a, plugin| 32 description: proc do |a, plugin|
33 description = a.body.to_s || a.environment.name 33 description = a.body.to_s || a.environment.name
34 - CGI.escapeHTML(plugin.helpers.truncate(plugin.helpers.strip_tags(description), length: 200)) 34 + plugin.helpers.truncate plugin.helpers.strip_tags(description), length: 200
35 end, 35 end,
36 title: proc{ |a, plugin| "#{a.title} - #{a.profile.name}" }, 36 title: proc{ |a, plugin| "#{a.title} - #{a.profile.name}" },
37 - image: proc{ |a, plugin| a.body_images_paths }, 37 + image: proc{ |a, plugin| a.body_images_paths.map! &:html_safe },
38 } 38 }
39 39
40 metadata_spec namespace: :article, key_attr: :property, tags: { 40 metadata_spec namespace: :article, key_attr: :property, tags: {
plugins/metadata/lib/ext/product.rb
@@ -14,8 +14,8 @@ class Product @@ -14,8 +14,8 @@ class Product
14 description: proc{ |p, plugin| ActionView::Base.full_sanitizer.sanitize p.description }, 14 description: proc{ |p, plugin| ActionView::Base.full_sanitizer.sanitize p.description },
15 15
16 image: proc do |p, plugin| 16 image: proc do |p, plugin|
17 - img = "#{p.environment.top_url}#{p.image.public_filename}" if p.image  
18 - img = "#{p.environment.top_url}#{p.profile.image.public_filename}" if img.blank? and p.profile.image 17 + img = "#{p.environment.top_url}#{p.image.public_filename}".html_safe if p.image
  18 + img = "#{p.environment.top_url}#{p.profile.image.public_filename}".html_safe if img.blank? and p.profile.image
19 img ||= MetadataPlugin.config[:open_graph][:environment_logo] rescue nil if img.blank? 19 img ||= MetadataPlugin.config[:open_graph][:environment_logo] rescue nil if img.blank?
20 img 20 img
21 end, 21 end,
plugins/metadata/lib/ext/profile.rb
@@ -5,8 +5,8 @@ class Profile @@ -5,8 +5,8 @@ class Profile
5 metadata_spec namespace: :og, tags: { 5 metadata_spec namespace: :og, tags: {
6 type: proc{ |p, plugin| plugin.context.params[:og_type] || MetadataPlugin.og_types[:profile] || :profile }, 6 type: proc{ |p, plugin| plugin.context.params[:og_type] || MetadataPlugin.og_types[:profile] || :profile },
7 image: proc do |p, plugin| 7 image: proc do |p, plugin|
8 - img = "#{p.environment.top_url}#{p.image.public_filename}" if p.image  
9 - img ||= MetadataPlugin.config[:open_graph][:environment_logo] rescue nil if img.blank? 8 + img = "#{p.environment.top_url}#{p.image.public_filename}".html_safe if p.image
  9 + img ||= MetadataPlugin.config[:open_graph][:environment_logo].html_safe rescue nil if img.blank?
10 img 10 img
11 end, 11 end,
12 title: proc{ |p, plugin| if p.nickname.present? then p.nickname else p.name end }, 12 title: proc{ |p, plugin| if p.nickname.present? then p.nickname else p.name end },
plugins/metadata/lib/ext/uploaded_file.rb
@@ -13,7 +13,7 @@ class UploadedFile @@ -13,7 +13,7 @@ class UploadedFile
13 plugin.og_url_for url 13 plugin.og_url_for url
14 end, 14 end,
15 title: proc{ |u, plugin| u.title }, 15 title: proc{ |u, plugin| u.title },
16 - image: proc{ |u, plugin| "#{u.environment.top_url}#{u.public_filename}" if u.image? }, 16 + image: proc{ |u, plugin| "#{u.environment.top_url}#{u.public_filename}".html_safe if u.image? },
17 description: proc{ |u, plugin| u.abstract || u.title }, 17 description: proc{ |u, plugin| u.abstract || u.title },
18 } 18 }
19 19
plugins/metadata/lib/metadata_plugin/base.rb
@@ -50,7 +50,8 @@ class MetadataPlugin::Base &lt; Noosfero::Plugin @@ -50,7 +50,8 @@ class MetadataPlugin::Base &lt; Noosfero::Plugin
50 Array(values).each do |value| 50 Array(values).each do |value|
51 value = value.call(object, plugin) if value.is_a? Proc rescue nil 51 value = value.call(object, plugin) if value.is_a? Proc rescue nil
52 next if value.blank? 52 next if value.blank?
53 - r << tag(:meta, key_attr => key, value_attr => CGI.escape_html(value.to_s)) 53 + value = h value unless value.html_safe?
  54 + r << tag(:meta, {key_attr => key, value_attr => value.to_s}, false, false)
54 end 55 end
55 end 56 end
56 end 57 end
plugins/metadata/lib/metadata_plugin/url_helper.rb
@@ -7,7 +7,8 @@ module MetadataPlugin::UrlHelper @@ -7,7 +7,8 @@ module MetadataPlugin::UrlHelper
7 def og_url_for options 7 def og_url_for options
8 options.delete :port 8 options.delete :port
9 options[:host] = self.og_domain 9 options[:host] = self.og_domain
10 - Noosfero::Application.routes.url_helpers.url_for options 10 + url = Noosfero::Application.routes.url_helpers.url_for options
  11 + url.html_safe
11 end 12 end
12 13
13 def og_profile_url profile 14 def og_profile_url profile
plugins/metadata/test/functional/content_viewer_controller_test.rb
@@ -45,6 +45,14 @@ class ContentViewerControllerTest &lt; ActionController::TestCase @@ -45,6 +45,14 @@ class ContentViewerControllerTest &lt; ActionController::TestCase
45 assert_tag tag: 'meta', attributes: { property: 'og:image', content: /\/images\/x.png/ } 45 assert_tag tag: 'meta', attributes: { property: 'og:image', content: /\/images\/x.png/ }
46 end 46 end
47 47
  48 + should 'escape utf8 characters correctly' do
  49 + a = TinyMceArticle.create(name: 'Article to be shared with images', body: 'This article should be shared with all social networks <img src="/images/ç.png" />', profile: profile)
  50 +
  51 + get :view_page, profile: profile.identifier, page: [ a.name.to_slug ]
  52 + assert_tag tag: 'meta', attributes: { property: 'og:image', content: /\/images\/%C3%A7.png/ }
  53 + end
  54 +
  55 +
48 should 'render not_found page properly' do 56 should 'render not_found page properly' do
49 assert_equal false, Article.exists?(:slug => 'non-existing-page') 57 assert_equal false, Article.exists?(:slug => 'non-existing-page')
50 assert_nothing_raised do 58 assert_nothing_raised do
plugins/oauth_client/controllers/public/oauth_client_plugin_public_controller.rb
@@ -4,8 +4,8 @@ class OauthClientPluginPublicController &lt; PublicController @@ -4,8 +4,8 @@ class OauthClientPluginPublicController &lt; PublicController
4 4
5 def callback 5 def callback
6 auth = request.env["omniauth.auth"] 6 auth = request.env["omniauth.auth"]
7 - user = environment.users.find_by_email(auth.info.email)  
8 - user ? login(user) : signup(auth) 7 + auth_user = environment.users.where(email: auth.info.email).first
  8 + if auth_user then login auth_user.person else signup auth end
9 end 9 end
10 10
11 def failure 11 def failure
@@ -20,14 +20,12 @@ class OauthClientPluginPublicController &lt; PublicController @@ -20,14 +20,12 @@ class OauthClientPluginPublicController &lt; PublicController
20 20
21 protected 21 protected
22 22
23 - def login(user) 23 + def login person
24 provider = OauthClientPlugin::Provider.find(session[:provider_id]) 24 provider = OauthClientPlugin::Provider.find(session[:provider_id])
25 - user_provider = user.oauth_user_providers.find_by_provider_id(provider.id)  
26 - unless user_provider  
27 - user_provider = user.oauth_user_providers.create(:user => user, :provider => provider, :enabled => true)  
28 - end  
29 - if user_provider.enabled? && provider.enabled?  
30 - session[:user] = user.id 25 + auth = person.oauth_auths.where(provider_id: provider.id).first
  26 + auth ||= person.oauth_auths.create! profile: person, provider: provider, enabled: true
  27 + if auth.enabled? && provider.enabled?
  28 + self.current_user = person.user
31 else 29 else
32 session[:notice] = _("Can't login with %s") % provider.name 30 session[:notice] = _("Can't login with %s") % provider.name
33 end 31 end
plugins/oauth_client/db/migrate/20150815170000_deserialize_fields_on_oauth_client_plugin_provider.rb 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +class DeserializeFieldsOnOauthClientPluginProvider < ActiveRecord::Migration
  2 +
  3 + def up
  4 + add_column :oauth_client_plugin_providers, :client_id, :text
  5 + add_column :oauth_client_plugin_providers, :client_secret, :text
  6 +
  7 + OauthClientPlugin::Provider.find_each batch_size: 50 do |provider|
  8 + provider.client_id = provider.options.delete :client_id
  9 + provider.client_secret = provider.options.delete :client_secret
  10 + provider.save!
  11 + end
  12 +
  13 + add_index :oauth_client_plugin_providers, :client_id
  14 + end
  15 +
  16 + def down
  17 + say "this migration can't be reverted"
  18 + end
  19 +
  20 +end
plugins/oauth_client/db/migrate/20150815173209_add_authorization_data_to_oauth_client_user_provider.rb 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +class AddAuthorizationDataToOauthClientUserProvider < ActiveRecord::Migration
  2 +
  3 + def change
  4 + rename_table :oauth_client_plugin_user_providers, :oauth_client_plugin_auths
  5 +
  6 + add_column :oauth_client_plugin_auths, :type, :string
  7 + add_column :oauth_client_plugin_auths, :provider_user_id, :string
  8 + add_column :oauth_client_plugin_auths, :access_token, :text
  9 + add_column :oauth_client_plugin_auths, :expires_at, :datetime
  10 + add_column :oauth_client_plugin_auths, :scope, :text
  11 + add_column :oauth_client_plugin_auths, :data, :text, default: {}.to_yaml
  12 +
  13 + add_column :oauth_client_plugin_auths, :profile_id, :integer
  14 + OauthClientPlugin::Auth.find_each batch_size: 50 do |auth|
  15 + auth.profile = User.find(auth.user_id).person
  16 + auth.save!
  17 + end
  18 + remove_column :oauth_client_plugin_auths, :user_id
  19 +
  20 + add_index :oauth_client_plugin_auths, :profile_id
  21 + add_index :oauth_client_plugin_auths, :provider_id
  22 + add_index :oauth_client_plugin_auths, :provider_user_id
  23 + add_index :oauth_client_plugin_auths, :type
  24 + end
  25 +
  26 +end
plugins/oauth_client/lib/ext/profile.rb 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +require_dependency 'profile'
  2 +
  3 +class Profile
  4 +
  5 + has_many :oauth_auths, class_name: 'OauthClientPlugin::Auth', dependent: :destroy
  6 + has_many :oauth_providers, through: :oauth_auths, source: :provider
  7 +
  8 +end
plugins/oauth_client/lib/ext/user.rb
@@ -2,8 +2,14 @@ require_dependency &#39;user&#39; @@ -2,8 +2,14 @@ require_dependency &#39;user&#39;
2 2
3 class User 3 class User
4 4
5 - has_many :oauth_user_providers, :class_name => 'OauthClientPlugin::UserProvider'  
6 - has_many :oauth_providers, :through => :oauth_user_providers, :source => :provider 5 + has_many :oauth_auths, through: :person
  6 + has_many :oauth_providers, through: :oauth_auths, source: :provider
  7 +
  8 + after_create :activate_oauth_user
  9 +
  10 + def activate_oauth_user
  11 + self.activate if oauth_providers.present?
  12 + end
7 13
8 def password_required_with_oauth? 14 def password_required_with_oauth?
9 password_required_without_oauth? && oauth_providers.empty? 15 password_required_without_oauth? && oauth_providers.empty?
@@ -11,17 +17,6 @@ class User @@ -11,17 +17,6 @@ class User
11 17
12 alias_method_chain :password_required?, :oauth 18 alias_method_chain :password_required?, :oauth
13 19
14 - after_create :activate_oauth_user  
15 -  
16 - def activate_oauth_user  
17 - unless oauth_providers.empty?  
18 - activate  
19 - oauth_providers.each do |provider|  
20 - OauthClientPlugin::UserProvider.create!(:user => self, :provider => provider, :enabled => true)  
21 - end  
22 - end  
23 - end  
24 -  
25 def make_activation_code_with_oauth 20 def make_activation_code_with_oauth
26 oauth_providers.blank? ? make_activation_code_without_oauth : nil 21 oauth_providers.blank? ? make_activation_code_without_oauth : nil
27 end 22 end
plugins/oauth_client/lib/oauth_client_plugin.rb
@@ -45,7 +45,9 @@ class OauthClientPlugin &lt; Noosfero::Plugin @@ -45,7 +45,9 @@ class OauthClientPlugin &lt; Noosfero::Plugin
45 true 45 true
46 end 46 end
47 47
48 - OmniAuth.config.on_failure = OauthClientPluginPublicController.action(:failure) 48 + Rails.configuration.to_prepare do
  49 + OmniAuth.config.on_failure = OauthClientPluginPublicController.action(:failure)
  50 + end
49 51
50 Rails.application.config.middleware.use OmniAuth::Builder do 52 Rails.application.config.middleware.use OmniAuth::Builder do
51 PROVIDERS.each do |provider, options| 53 PROVIDERS.each do |provider, options|
@@ -60,7 +62,8 @@ class OauthClientPlugin &lt; Noosfero::Plugin @@ -60,7 +62,8 @@ class OauthClientPlugin &lt; Noosfero::Plugin
60 provider_id = request.params['id'] 62 provider_id = request.params['id']
61 provider_id ||= request.session['omniauth.params']['id'] if request.session['omniauth.params'] 63 provider_id ||= request.session['omniauth.params']['id'] if request.session['omniauth.params']
62 provider = environment.oauth_providers.find(provider_id) 64 provider = environment.oauth_providers.find(provider_id)
63 - strategy.options.merge!(provider.options.symbolize_keys) 65 + strategy.options.merge! client_id: provider.client_id, client_secret: provider.client_secret
  66 + strategy.options.merge! provider.options.symbolize_keys
64 67
65 request.session[:provider_id] = provider_id 68 request.session[:provider_id] = provider_id
66 } 69 }
plugins/oauth_client/lib/oauth_client_plugin/provider.rb
@@ -1,20 +0,0 @@ @@ -1,20 +0,0 @@
1 -class OauthClientPlugin::Provider < ActiveRecord::Base  
2 -  
3 - belongs_to :environment  
4 -  
5 - validates_presence_of :name, :strategy  
6 -  
7 - acts_as_having_image  
8 - acts_as_having_settings :field => :options  
9 -  
10 - settings_items :client_id, :type => :string  
11 - settings_items :client_secret, :type => :string  
12 - settings_items :client_options, :type => Hash  
13 -  
14 - attr_accessible :name, :environment, :strategy, :client_id, :client_secret, :enabled, :client_options, :image_builder  
15 -  
16 - scope :enabled, -> { where enabled: true }  
17 -  
18 - acts_as_having_image  
19 -  
20 -end  
plugins/oauth_client/lib/oauth_client_plugin/user_provider.rb
@@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
1 -class OauthClientPlugin::UserProvider < ActiveRecord::Base  
2 -  
3 - belongs_to :user, :class_name => 'User'  
4 - belongs_to :provider, :class_name => 'OauthClientPlugin::Provider'  
5 -  
6 - self.table_name = :oauth_client_plugin_user_providers  
7 -  
8 - attr_accessible :user, :provider, :enabled  
9 -  
10 -end  
plugins/oauth_client/models/oauth_client_plugin/auth.rb 0 → 100644
@@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
  1 +class OauthClientPlugin::Auth < ActiveRecord::Base
  2 +
  3 + attr_accessible :profile, :provider, :enabled,
  4 + :access_token, :expires_in
  5 +
  6 + belongs_to :profile, class_name: 'Profile'
  7 + belongs_to :provider, class_name: 'OauthClientPlugin::Provider'
  8 +
  9 + validates_presence_of :profile
  10 + validates_presence_of :provider
  11 + validates_uniqueness_of :profile_id, scope: :provider_id
  12 +
  13 + acts_as_having_settings field: :data
  14 +
  15 + def expires_in
  16 + self.expires_at - Time.now
  17 + end
  18 + def expires_in= value
  19 + self.expires_at = Time.now + value.to_i
  20 + end
  21 +
  22 + def expired?
  23 + Time.now > self.expires_at rescue true
  24 + end
  25 + def not_expired?
  26 + not self.expired?
  27 + end
  28 +
  29 +end
plugins/oauth_client/models/oauth_client_plugin/provider.rb 0 → 100644
@@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
  1 +class OauthClientPlugin::Provider < ActiveRecord::Base
  2 +
  3 + belongs_to :environment
  4 +
  5 + validates_presence_of :name, :strategy
  6 +
  7 + acts_as_having_image
  8 + acts_as_having_settings field: :options
  9 +
  10 + settings_items :site, type: String
  11 + settings_items :client_options, type: Hash
  12 +
  13 + attr_accessible :name, :strategy, :enabled, :site, :image_builder,
  14 + :environment, :environment_id,
  15 + :client_id, :client_secret, :client_options
  16 +
  17 + scope :enabled, -> { where enabled: true }
  18 +
  19 + acts_as_having_image
  20 +
  21 +end
plugins/oauth_client/test/functional/oauth_client_plugin_public_controller_test.rb
1 -require_relative '../test_helper' 1 +require 'test_helper'
2 2
3 class OauthClientPluginPublicControllerTest < ActionController::TestCase 3 class OauthClientPluginPublicControllerTest < ActionController::TestCase
4 4
@@ -21,7 +21,7 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase @@ -21,7 +21,7 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase
21 end 21 end
22 22
23 should 'redirect to login when user is found' do 23 should 'redirect to login when user is found' do
24 - user = fast_create(User, :environment_id => environment.id) 24 + user = create_user
25 auth.info.stubs(:email).returns(user.email) 25 auth.info.stubs(:email).returns(user.email)
26 auth.info.stubs(:name).returns(user.name) 26 auth.info.stubs(:name).returns(user.name)
27 session[:provider_id] = provider.id 27 session[:provider_id] = provider.id
@@ -32,7 +32,7 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase @@ -32,7 +32,7 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase
32 end 32 end
33 33
34 should 'do not login when the provider is disabled' do 34 should 'do not login when the provider is disabled' do
35 - user = fast_create(User, :environment_id => environment.id) 35 + user = create_user
36 auth.info.stubs(:email).returns(user.email) 36 auth.info.stubs(:email).returns(user.email)
37 auth.info.stubs(:name).returns(user.name) 37 auth.info.stubs(:name).returns(user.name)
38 session[:provider_id] = provider.id 38 session[:provider_id] = provider.id
@@ -44,11 +44,11 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase @@ -44,11 +44,11 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase
44 end 44 end
45 45
46 should 'do not login when the provider is disabled for a user' do 46 should 'do not login when the provider is disabled for a user' do
47 - user = fast_create(User, :environment_id => environment.id) 47 + user = create_user
48 auth.info.stubs(:email).returns(user.email) 48 auth.info.stubs(:email).returns(user.email)
49 auth.info.stubs(:name).returns(user.name) 49 auth.info.stubs(:name).returns(user.name)
50 session[:provider_id] = provider.id 50 session[:provider_id] = provider.id
51 - user.oauth_user_providers.create(:user => user, :provider => provider, :enabled => false) 51 + user.person.oauth_auths.create!(profile: user.person, provider: provider, enabled: false)
52 52
53 get :callback 53 get :callback
54 assert_redirected_to :controller => :account, :action => :login 54 assert_redirected_to :controller => :account, :action => :login
@@ -56,7 +56,7 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase @@ -56,7 +56,7 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase
56 end 56 end
57 57
58 should 'save provider when an user login with it' do 58 should 'save provider when an user login with it' do
59 - user = fast_create(User, :environment_id => environment.id) 59 + user = create_user
60 auth.info.stubs(:email).returns(user.email) 60 auth.info.stubs(:email).returns(user.email)
61 auth.info.stubs(:name).returns(user.name) 61 auth.info.stubs(:name).returns(user.name)
62 session[:provider_id] = provider.id 62 session[:provider_id] = provider.id
@@ -66,13 +66,13 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase @@ -66,13 +66,13 @@ class OauthClientPluginPublicControllerTest &lt; ActionController::TestCase
66 end 66 end
67 67
68 should 'do not duplicate relations between an user and a provider when the same provider was used again in a login' do 68 should 'do not duplicate relations between an user and a provider when the same provider was used again in a login' do
69 - user = fast_create(User, :environment_id => environment.id) 69 + user = create_user
70 auth.info.stubs(:email).returns(user.email) 70 auth.info.stubs(:email).returns(user.email)
71 auth.info.stubs(:name).returns(user.name) 71 auth.info.stubs(:name).returns(user.name)
72 session[:provider_id] = provider.id 72 session[:provider_id] = provider.id
73 73
74 get :callback 74 get :callback
75 - assert_no_difference 'user.oauth_user_providers.count' do 75 + assert_no_difference 'user.oauth_auths.count' do
76 3.times { get :callback } 76 3.times { get :callback }
77 end 77 end
78 end 78 end
plugins/oauth_client/test/unit/environment_test.rb
1 -require_relative '../test_helper' 1 +require 'test_helper'
2 2
3 class UserTest < ActiveSupport::TestCase 3 class UserTest < ActiveSupport::TestCase
4 4
plugins/oauth_client/test/unit/oauth_client_plugin_test.rb
1 -require_relative '../test_helper' 1 +require 'test_helper'
2 2
3 class OauthClientPluginTest < ActiveSupport::TestCase 3 class OauthClientPluginTest < ActiveSupport::TestCase
4 4
plugins/oauth_client/test/unit/user_test.rb
1 -require_relative '../test_helper' 1 +require 'test_helper'
2 2
3 class UserTest < ActiveSupport::TestCase 3 class UserTest < ActiveSupport::TestCase
4 4
plugins/open_graph/db/migrate/20150814200324_add_story_and_published_at_to_open_graph_plugin_activity.rb 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +class AddStoryAndPublishedAtToOpenGraphPluginActivity < ActiveRecord::Migration
  2 +
  3 + def change
  4 + add_column :open_graph_plugin_tracks, :published_at, :datetime
  5 + add_column :open_graph_plugin_tracks, :story, :string
  6 + add_index :open_graph_plugin_tracks, :published_at
  7 + add_index :open_graph_plugin_tracks, :story
  8 + end
  9 +
  10 +end
plugins/open_graph/lib/open_graph_plugin.rb
@@ -17,5 +17,9 @@ module OpenGraphPlugin @@ -17,5 +17,9 @@ module OpenGraphPlugin
17 Thread.current[:open_graph_context] = value 17 Thread.current[:open_graph_context] = value
18 end 18 end
19 19
  20 + def self.debug? actor=nil
  21 + !Rails.env.production?
  22 + end
  23 +
20 end 24 end
21 25
plugins/open_graph/lib/open_graph_plugin/publisher.rb
1 1
2 class OpenGraphPlugin::Publisher 2 class OpenGraphPlugin::Publisher
3 3
4 - attr_accessor :actions  
5 - attr_accessor :objects  
6 -  
7 def self.default 4 def self.default
8 @default ||= self.new 5 @default ||= self.new
9 end 6 end
10 7
11 def initialize attributes = {} 8 def initialize attributes = {}
12 - # defaults  
13 - self.actions = OpenGraphPlugin::Stories::DefaultActions  
14 - self.objects = OpenGraphPlugin::Stories::DefaultObjects  
15 -  
16 attributes.each do |attr, value| 9 attributes.each do |attr, value|
17 self.send "#{attr}=", value 10 self.send "#{attr}=", value
18 end 11 end
19 end 12 end
20 13
21 - def publish actor, story_defs, object_data_url  
22 - raise 'abstract method called'  
23 - end  
24 -  
25 def publish_stories object_data, actor, stories 14 def publish_stories object_data, actor, stories
26 stories.each do |story| 15 stories.each do |story|
27 begin 16 begin
28 self.publish_story object_data, actor, story 17 self.publish_story object_data, actor, story
29 rescue => e 18 rescue => e
  19 + raise unless Rails.env.production?
30 ExceptionNotifier.notify_exception e 20 ExceptionNotifier.notify_exception e
31 end 21 end
32 end 22 end
33 end 23 end
34 24
35 - def update_delay  
36 - 1.day  
37 - end  
38 -  
39 - # only publish recent objects to avoid multiple publications  
40 - def recent_publish? actor, object_type, object_data_url  
41 - activity_params = {actor_id: actor.id, object_type: object_type, object_data_url: object_data_url}  
42 - activity = OpenGraphPlugin::Activity.where(activity_params).first  
43 - activity.present? and activity.created_at <= self.update_delay.from_now  
44 - end  
45 -  
46 def publish_story object_data, actor, story 25 def publish_story object_data, actor, story
47 - OpenGraphPlugin.context = self.context  
48 - defs = OpenGraphPlugin::Stories::Definitions[story]  
49 - passive = defs[:passive]  
50 -  
51 - print_debug "open_graph: publish_story #{story}" if debug? actor  
52 - match_criteria = if (ret = self.call defs[:criteria], object_data, actor).nil? then true else ret end  
53 - return unless match_criteria  
54 - print_debug "open_graph: #{story} match criteria" if debug? actor  
55 - match_condition = if (ret = self.call defs[:publish_if], object_data, actor).nil? then true else ret end  
56 - return unless match_condition  
57 - print_debug "open_graph: #{story} match publish_if" if debug? actor  
58 -  
59 - actors = self.story_trackers defs, actor, object_data  
60 - return if actors.blank?  
61 - print_debug "open_graph: #{story} has enabled trackers" if debug? actor  
62 -  
63 - if publish = defs[:publish]  
64 - begin  
65 - instance_exec actor, object_data, &publish  
66 - rescue => e  
67 - print_debug "open_graph: can't publish story: #{e.message}" if debug? actor  
68 - ExceptionNotifier.notify_exception e  
69 - end  
70 - else  
71 - # force profile identifier for custom domains and fixed host. see og_url_for  
72 - object_profile = self.call(story_defs[:object_profile], object_data) || object_data.profile rescue nil  
73 - extra_params = if object_profile then {profile: object_profile.identifier} else {} end  
74 -  
75 - custom_object_data_url = self.call defs[:object_data_url], object_data, actor  
76 - object_data_url = if passive then self.passive_url_for object_data, custom_object_data_url, defs, extra_params else self.url_for object_data, custom_object_data_url, extra_params end  
77 -  
78 - actors.each do |actor|  
79 - print_debug "open_graph: start publishing" if debug? actor  
80 - begin  
81 - self.publish actor, defs, object_data_url  
82 - rescue => e  
83 - print_debug "open_graph: can't publish story: #{e.message}" if debug? actor  
84 - ExceptionNotifier.notify_exception e  
85 - end  
86 - end  
87 - end  
88 - end  
89 -  
90 - def story_trackers story_defs, actor, object_data  
91 - passive = story_defs[:passive]  
92 - trackers = []  
93 -  
94 - track_configs = Array(story_defs[:track_config]).compact.map(&:constantize)  
95 - return if track_configs.empty?  
96 - print_debug "open_graph: using configs: #{track_configs.map(&:name).inspect}" if debug? actor  
97 -  
98 - if passive  
99 - object_profile = self.call(story_defs[:object_profile], object_data) || object_data.profile rescue nil  
100 - return unless object_profile  
101 -  
102 - track_configs.each do |c|  
103 - trackers.concat c.trackers_to_profile(object_profile)  
104 - end.flatten  
105 -  
106 - trackers.select! do |t|  
107 - track_configs.any?{ |c| c.enabled? self.context, t }  
108 - end  
109 - else #active  
110 - object_actor = self.call(story_defs[:object_actor], object_data) || object_data.profile rescue nil  
111 - return unless object_actor and object_actor.person?  
112 - custom_actor = self.call(story_defs[:custom_actor], object_data)  
113 - actor = custom_actor if custom_actor  
114 -  
115 - match_track = track_configs.any? do |c|  
116 - c.enabled?(self.context, actor) and  
117 - actor.send("open_graph_#{c.track_name}_track_configs").where(object_type: story_defs[:object_type]).first  
118 - end  
119 - trackers << actor if match_track  
120 - end  
121 -  
122 - trackers 26 + OpenGraphPlugin.context = OpenGraphPlugin::Activity.context
  27 + a = OpenGraphPlugin::Activity.new object_data: object_data, actor: actor, story: story
  28 + a.dispatch_publications
  29 + a.save
123 end 30 end
124 31
125 protected 32 protected
126 33
127 - include MetadataPlugin::UrlHelper  
128 -  
129 - def register_publish attributes  
130 - OpenGraphPlugin::Activity.create! attributes  
131 - end  
132 -  
133 - # Call don't ask: move to a og_url method inside object  
134 - def url_for object, custom_url=nil, extra_params={}  
135 - return custom_url if custom_url.is_a? String  
136 - url = custom_url || if object.is_a? Profile then og_profile_url object else object.url end  
137 - # for profile when custom domain is used  
138 - url.merge! profile: object.profile.identifier if object.respond_to? :profile  
139 - url.merge! extra_params  
140 - self.og_url_for url  
141 - end  
142 -  
143 - def passive_url_for object, custom_url, story_defs, extra_params={}  
144 - object_type = story_defs[:object_type]  
145 - extra_params.merge! og_type: MetadataPlugin.og_types[object_type]  
146 - self.url_for object, custom_url, extra_params  
147 - end  
148 -  
149 - def call p, *args  
150 - p and instance_exec *args, &p  
151 - end  
152 -  
153 - def context  
154 - :open_graph  
155 - end  
156 -  
157 - def print_debug msg  
158 - puts msg  
159 - Delayed::Worker.logger.debug msg  
160 - end  
161 - def debug? actor=nil  
162 - !Rails.env.production?  
163 - end  
164 -  
165 end 34 end
166 35
plugins/open_graph/lib/open_graph_plugin/stories.rb
@@ -99,13 +99,6 @@ class OpenGraphPlugin::Stories @@ -99,13 +99,6 @@ class OpenGraphPlugin::Stories
99 publish_if: proc do |article, actor| 99 publish_if: proc do |article, actor|
100 article.published? 100 article.published?
101 end, 101 end,
102 - object_data_url: proc do |article, actor|  
103 - url = article.url  
104 - if og_type = MetadataPlugin::og_types[:forum]  
105 - url[:og_type] = og_type  
106 - end  
107 - url  
108 - end,  
109 }, 102 },
110 103
111 # these a published as passive to give focus to the enterprise 104 # these a published as passive to give focus to the enterprise
plugins/open_graph/lib/open_graph_plugin/url_helper.rb 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +module OpenGraphPlugin::UrlHelper
  2 +
  3 + protected
  4 +
  5 + include MetadataPlugin::UrlHelper
  6 +
  7 + # Call don't ask: move to a og_url method inside object
  8 + def url_for object, custom_url=nil, extra_params={}
  9 + return custom_url if custom_url.is_a? String
  10 + url = custom_url || if object.is_a? Profile then og_profile_url object else object.url end
  11 + # for profile when custom domain is used
  12 + url.merge! profile: object.profile.identifier if object.respond_to? :profile
  13 + url.merge! extra_params
  14 + self.og_url_for url
  15 + end
  16 +
  17 + def passive_url_for object, custom_url, story_defs, extra_params={}
  18 + object_type = story_defs[:object_type]
  19 + og_type = MetadataPlugin.og_types[object_type]
  20 + extra_params.merge! og_type: og_type if og_type.present?
  21 + self.url_for object, custom_url, extra_params
  22 + end
  23 +
  24 +end
plugins/open_graph/models/open_graph_plugin/activity.rb
1 # This is a log of activities, unlike ActivityTrack that is a configuration 1 # This is a log of activities, unlike ActivityTrack that is a configuration
2 class OpenGraphPlugin::Activity < OpenGraphPlugin::Track 2 class OpenGraphPlugin::Activity < OpenGraphPlugin::Track
3 3
  4 + Defs = OpenGraphPlugin::Stories::Definitions
  5 +
  6 + UpdateDelay = 1.day
  7 +
  8 + class_attribute :actions, :objects
  9 + self.actions = OpenGraphPlugin::Stories::DefaultActions
  10 + self.objects = OpenGraphPlugin::Stories::DefaultObjects
  11 +
  12 + validates_presence_of :action
  13 + validates_presence_of :object_type
  14 +
4 # subclass this to define (e.g. FbAppPlugin::Activity) 15 # subclass this to define (e.g. FbAppPlugin::Activity)
5 def scrape 16 def scrape
  17 + raise NotImplementedError
  18 + end
  19 + def publish! actor = self.actor
  20 + self.published_at = Time.now
  21 + print_debug "open_graph: published with success" if debug? actor
  22 + end
  23 +
  24 + def defs
  25 + @defs ||= Defs[self.story.to_sym]
  26 + end
  27 + def object_profile
  28 + @object_profile ||= self.call(self.defs[:object_profile], self.object_data) || self.object_data.profile rescue nil
  29 + end
  30 + def track_configs
  31 + @track_configs ||= Array(self.defs[:track_config]).compact.map(&:constantize)
  32 + end
  33 + def match_criteria?
  34 + if (ret = self.call self.defs[:criteria], self.object_data, self.actor).nil? then true else ret end
  35 + end
  36 + def match_publish_if?
  37 + if (ret = self.call self.defs[:publish_if], self.object_data, self.actor).nil? then true else ret end
  38 + end
  39 + def custom_object_data_url
  40 + @custom_object_data_url ||= self.call defs[:object_data_url], self.object_data, self.actor
  41 + end
  42 + def object_actor
  43 + @object_actor ||= self.call(self.defs[:object_actor], self.object_data) || self.object_data.profile rescue nil
  44 + end
  45 + def custom_actor
  46 + @custom_actor ||= self.call self.defs[:custom_actor], self.object_data
  47 + end
  48 +
  49 + def set_object_data_url
  50 + # force profile identifier for custom domains and fixed host. see og_url_for
  51 + extra_params = if self.object_profile then {profile: self.object_profile.identifier} else {} end
  52 +
  53 + self.object_data_url = if self.defs[:passive] then self.passive_url_for self.object_data, self.custom_object_data_url, self.defs, extra_params else self.url_for self.object_data, self.custom_object_data_url, extra_params end
  54 + end
  55 +
  56 + def dispatch_publications
  57 + print_debug "open_graph: dispatch_publications of #{story}" if debug? self.actor
  58 +
  59 + return unless self.match_criteria?
  60 + print_debug "open_graph: #{story} match criteria" if debug? self.actor
  61 + return unless self.match_publish_if?
  62 + print_debug "open_graph: #{story} match publish_if" if debug? self.actor
  63 + return unless (actors = self.trackers).present?
  64 + print_debug "open_graph: #{story} has enabled trackers" if debug? self.actor
  65 +
  66 + self.set_object_data_url
  67 + self.action = self.class.actions[self.defs[:action]]
  68 + self.object_type = self.class.objects[self.defs[:object_type]]
  69 +
  70 + print_debug "open_graph: start publishing" if debug? actor
  71 + unless (publish = self.defs[:publish]).present?
  72 + actors.each do |actor|
  73 + begin
  74 + self.publish! actor
  75 + rescue => e
  76 + print_debug "open_graph: can't publish story: #{e.message}" if debug? actor
  77 + raise unless Rails.env.production?
  78 + ExceptionNotifier.notify_exception e
  79 + end
  80 + end
  81 + else # custom publish proc
  82 + begin
  83 + instance_exec self.actor, self.object_data, &publish
  84 + rescue => e
  85 + print_debug "open_graph: can't publish story: #{e.message}" if debug? self.actor
  86 + raise unless Rails.env.production?
  87 + ExceptionNotifier.notify_exception e
  88 + end
  89 + end
  90 + end
  91 +
  92 + def trackers
  93 + @trackers ||= begin
  94 + return if self.track_configs.empty?
  95 + trackers = []
  96 +
  97 + print_debug "open_graph: using configs: #{self.track_configs.map(&:name).inspect}" if debug? self.actor
  98 +
  99 + if self.defs[:passive]
  100 + return unless self.object_profile
  101 +
  102 + self.track_configs.each do |c|
  103 + trackers.concat c.trackers_to_profile(self.object_profile)
  104 + end.flatten
  105 +
  106 + trackers.select! do |t|
  107 + self.track_configs.any?{ |c| c.enabled? self.context, t }
  108 + end
  109 + else #active
  110 + return unless self.object_actor and self.object_actor.person?
  111 + actor = self.custom_actor || self.actor
  112 +
  113 + match_track = self.track_configs.any? do |c|
  114 + c.enabled?(self.context, actor) and
  115 + actor.send("open_graph_#{c.track_name}_track_configs").where(object_type: self.defs[:object_type]).first
  116 + end
  117 + trackers << actor if match_track
  118 + end
  119 +
  120 + trackers
  121 + end
  122 + end
  123 +
  124 + protected
  125 +
  126 + include OpenGraphPlugin::UrlHelper
  127 +
  128 + def update_delay
  129 + UpdateDelay
  130 + end
  131 +
  132 + # only publish recent objects to avoid multiple publications
  133 + def recent_publish? actor, object_type, object_data_url
  134 + activity_params = {actor_id: actor.id, object_type: object_type, object_data_url: object_data_url}
  135 + activity = OpenGraphPlugin::Activity.where(activity_params).first
  136 + activity.present? and activity.created_at <= self.update_delay.from_now
  137 + end
  138 +
  139 + def call p, *args
  140 + p and instance_exec *args, &p
6 end 141 end
7 142
8 end 143 end
plugins/open_graph/models/open_graph_plugin/track.rb
1 class OpenGraphPlugin::Track < ActiveRecord::Base 1 class OpenGraphPlugin::Track < ActiveRecord::Base
2 2
  3 + class_attribute :context
  4 + self.context = :open_graph
  5 +
3 attr_accessible :type, :context, :tracker_id, :tracker, :actor_id, :action, 6 attr_accessible :type, :context, :tracker_id, :tracker, :actor_id, :action,
4 - :object_type, :object_data, :object_data_id, :object_data_type, :object_data_url 7 + :object_type, :object_data_id, :object_data_type, :object_data_url,
  8 + :story, :object_data, :actor
5 9
6 belongs_to :tracker, class_name: 'Profile' 10 belongs_to :tracker, class_name: 'Profile'
7 belongs_to :actor, class_name: 'Profile' 11 belongs_to :actor, class_name: 'Profile'
8 belongs_to :object_data, polymorphic: true 12 belongs_to :object_data, polymorphic: true
9 13
10 - validates_presence_of :context  
11 before_validation :set_context 14 before_validation :set_context
12 15
13 def self.objects 16 def self.objects
@@ -21,7 +24,15 @@ class OpenGraphPlugin::Track &lt; ActiveRecord::Base @@ -21,7 +24,15 @@ class OpenGraphPlugin::Track &lt; ActiveRecord::Base
21 protected 24 protected
22 25
23 def set_context 26 def set_context
24 - self.context = OpenGraphPlugin.context 27 + self[:context] = self.class.context
  28 + end
  29 +
  30 + def print_debug msg
  31 + puts msg
  32 + Delayed::Worker.logger.debug msg
  33 + end
  34 + def debug? actor=nil
  35 + OpenGraphPlugin.debug? actor
25 end 36 end
26 37
27 end 38 end
plugins/open_graph/plugin.yml
@@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
1 -name: open_graph  
2 -dependencies:  
3 - - metadata  
plugins/open_graph/test/unit/open_graph_graph/publisher_test.rb
@@ -2,14 +2,16 @@ require &quot;test_helper&quot; @@ -2,14 +2,16 @@ require &quot;test_helper&quot;
2 2
3 class OpenGraphPlugin::PublisherTest < ActiveSupport::TestCase 3 class OpenGraphPlugin::PublisherTest < ActiveSupport::TestCase
4 4
  5 + include OpenGraphPlugin::UrlHelper
  6 +
5 def setup 7 def setup
6 @actor = create_user.person 8 @actor = create_user.person
7 User.current = @actor.user 9 User.current = @actor.user
8 - @stories = OpenGraphPlugin::Stories::Definitions  
9 @publisher = OpenGraphPlugin::Publisher.new 10 @publisher = OpenGraphPlugin::Publisher.new
10 OpenGraphPlugin::Stories.stubs(:publishers).returns([@publisher]) 11 OpenGraphPlugin::Stories.stubs(:publishers).returns([@publisher])
11 - @publisher.stubs(:context).returns(:open_graph)  
12 - @publisher.stubs(:og_domain).returns('noosfero.net') 12 + # for MetadataPlugin::UrlHelper#og_url_for
  13 + stubs(:og_domain).returns('noosfero.net')
  14 + OpenGraphPlugin::Activity.any_instance.stubs(:og_domain).returns('noosfero.net')
13 end 15 end
14 16
15 should "publish only tracked stuff" do 17 should "publish only tracked stuff" do
@@ -46,66 +48,70 @@ class OpenGraphPlugin::PublisherTest &lt; ActiveSupport::TestCase @@ -46,66 +48,70 @@ class OpenGraphPlugin::PublisherTest &lt; ActiveSupport::TestCase
46 48
47 # active 49 # active
48 User.current = @actor.user 50 User.current = @actor.user
  51 + user = User.current.person
  52 +
  53 + blog = Blog.create! profile: user, name: 'blog'
  54 + blog_post = TinyMceArticle.create! profile: user, parent: blog, name: 'blah', author: user
  55 + assert_last_activity user, :create_an_article, url_for(blog_post)
  56 +
  57 + gallery = Gallery.create! name: 'gallery', profile: user
  58 + image = UploadedFile.create! uploaded_data: fixture_file_upload('/files/rails.png', 'image/png'), parent: gallery, profile: user
  59 + assert_last_activity user, :add_an_image, url_for(image, image.url.merge(view: true))
  60 +
  61 + document = UploadedFile.create! uploaded_data: fixture_file_upload('/files/doctest.en.xhtml', 'text/html'), profile: user
  62 + assert_last_activity user, :add_a_document, url_for(document, document.url.merge(view: true))
  63 +
  64 + event = Event.create! name: 'event', profile: user
  65 + assert_last_activity user, :create_an_event, url_for(event)
  66 +
  67 + forum = Forum.create! name: 'forum', profile: user
  68 + topic = TinyMceArticle.create! profile: user, parent: forum, name: 'blah2', author: user
  69 + assert_last_activity user, :start_a_discussion, url_for(topic, topic.url.merge(og_type: MetadataPlugin.og_types[:forum]))
49 70
50 - blog = Blog.create! profile: @actor, name: 'blog'  
51 - blog_post = TinyMceArticle.new profile: User.current.person, parent: blog, name: 'blah', author: User.current.person  
52 - @publisher.expects(:publish).with(User.current.person, @stories[:create_an_article], @publisher.send(:url_for, blog_post))  
53 - blog_post.save!  
54 -  
55 - gallery = Gallery.create! name: 'gallery', profile: User.current.person  
56 - image = UploadedFile.new uploaded_data: fixture_file_upload('/files/rails.png', 'image/png'), parent: gallery, profile: User.current.person  
57 - @publisher.expects(:publish).with(User.current.person, @stories[:add_an_image], @publisher.send(:url_for, image, image.url.merge(view: true)))  
58 - image.save!  
59 -  
60 - document = UploadedFile.new uploaded_data: fixture_file_upload('/files/doctest.en.xhtml', 'text/html'), profile: User.current.person  
61 - @publisher.expects(:publish).with(User.current.person, @stories[:add_a_document], @publisher.send(:url_for, document, document.url.merge(view: true)))  
62 - document.save!  
63 -  
64 - event = Event.new name: 'event', profile: User.current.person  
65 - @publisher.expects(:publish).with(User.current.person, @stories[:create_an_event], @publisher.send(:url_for, event))  
66 - event.save!  
67 -  
68 - forum = Forum.create! name: 'forum', profile: User.current.person  
69 - topic = TinyMceArticle.new profile: User.current.person, parent: forum, name: 'blah2', author: User.current.person  
70 - @publisher.expects(:publish).with(User.current.person, @stories[:start_a_discussion], @publisher.send(:url_for, topic, topic.url.merge(og_type: MetadataPlugin.og_types[:forum])))  
71 - topic.save!  
72 -  
73 - @publisher.expects(:publish).with(@actor, @stories[:make_friendship_with], @publisher.send(:url_for, @other_actor)).twice  
74 - @publisher.expects(:publish).with(@other_actor, @stories[:make_friendship_with], @publisher.send(:url_for, @actor)).twice  
75 - AddFriend.create!(person: @actor, friend: @other_actor).finish  
76 - Friendship.remove_friendship @actor, @other_actor 71 + AddFriend.create!(person: user, friend: @other_actor).finish
  72 + #assert_last_activity user, :make_friendship_with, url_for(@other_actor)
  73 + Friendship.remove_friendship user, @other_actor
77 # friend verb is groupable 74 # friend verb is groupable
78 - AddFriend.create!(person: @actor, friend: @other_actor).finish 75 + AddFriend.create!(person: user, friend: @other_actor).finish
  76 + #assert_last_activity @other_actor, :make_friendship_with, url_for(user)
79 77
80 - @publisher.expects(:publish).with(User.current.person, @stories[:favorite_a_sse_initiative], @publisher.send(:url_for, @enterprise))  
81 - @enterprise.fans << User.current.person 78 + @enterprise.fans << user
  79 + assert_last_activity user, :favorite_a_sse_initiative, url_for(@enterprise)
82 80
83 # active but published as passive 81 # active but published as passive
84 User.current = @actor.user 82 User.current = @actor.user
  83 + user = User.current.person
85 84
86 - blog_post = TinyMceArticle.new profile: @enterprise, parent: @enterprise.blog, name: 'blah', author: User.current.person  
87 - story = @stories[:announce_news_from_a_sse_initiative]  
88 - @publisher.expects(:publish).with(User.current.person, story, @publisher.send(:passive_url_for, blog_post, nil, story))  
89 - blog_post.save! 85 + blog_post = TinyMceArticle.create! profile: @enterprise, parent: @enterprise.blog, name: 'blah', author: user
  86 + story = :announce_news_from_a_sse_initiative
  87 + assert_last_activity user, story, passive_url_for(blog_post, nil, OpenGraphPlugin::Stories::Definitions[story])
90 88
91 # passive 89 # passive
92 User.current = @other_actor.user 90 User.current = @other_actor.user
  91 + user = User.current.person
93 92
94 # fan 93 # fan
95 - blog_post = TinyMceArticle.new profile: @enterprise, parent: @enterprise.blog, name: 'blah2', author: User.current.person  
96 - story = @stories[:announce_news_from_a_sse_initiative]  
97 - @publisher.expects(:publish).with(@actor, story, 'http://noosfero.net/coop/blog/blah2')  
98 - blog_post.save! 94 + blog_post = TinyMceArticle.create! profile: @enterprise, parent: @enterprise.blog, name: 'blah2', author: user
  95 + assert_last_activity user, :announce_news_from_a_sse_initiative, 'http://noosfero.net/coop/blog/blah2'
99 # member 96 # member
100 - blog_post = TinyMceArticle.new profile: @myenterprise, parent: @myenterprise.blog, name: 'blah2', author: User.current.person  
101 - story = @stories[:announce_news_from_a_sse_initiative]  
102 - @publisher.expects(:publish).with(@actor, story, 'http://noosfero.net/mycoop/blog/blah2')  
103 - blog_post.save!  
104 -  
105 - blog_post = TinyMceArticle.new profile: @community, parent: @community.blog, name: 'blah', author: User.current.person  
106 - story = @stories[:announce_news_from_a_community]  
107 - @publisher.expects(:publish).with(@actor, story, 'http://noosfero.net/comm/blog/blah')  
108 - blog_post.save! 97 + blog_post = TinyMceArticle.create! profile: @myenterprise, parent: @myenterprise.blog, name: 'blah2', author: user
  98 + assert_last_activity user, :announce_news_from_a_sse_initiative, 'http://noosfero.net/mycoop/blog/blah2'
  99 +
  100 + blog_post = TinyMceArticle.create! profile: @community, parent: @community.blog, name: 'blah', author: user
  101 + assert_last_activity user, :announce_news_from_a_community, 'http://noosfero.net/comm/blog/blah'
  102 + end
  103 +
  104 + protected
  105 +
  106 + def assert_activity activity, actor, story, object_data_url
  107 + assert_equal actor, activity.actor, actor
  108 + assert_equal story.to_s, activity.story
  109 + assert_equal object_data_url, activity.object_data_url
  110 + end
  111 +
  112 + def assert_last_activity actor, story, object_data_url
  113 + a = OpenGraphPlugin::Activity.order('id DESC').first
  114 + assert_activity a, actor, story, object_data_url
109 end 115 end
110 116
111 end 117 end
plugins/site_tour/controllers/public/site_tour_plugin_public_controller.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +class SiteTourPluginPublicController < PublicController
  2 +
  3 + before_filter :login_required
  4 +
  5 + def mark_action
  6 + user.site_tour_plugin_actions += [params[:action_name]].flatten
  7 + user.site_tour_plugin_actions.uniq!
  8 + render :json => {:ok => user.save}
  9 + end
  10 +
  11 +end
plugins/site_tour/controllers/site_tour_plugin_admin_controller.rb 0 → 100644
@@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
  1 +require 'csv'
  2 +
  3 +class SiteTourPluginAdminController < PluginAdminController
  4 +
  5 + no_design_blocks
  6 +
  7 + def index
  8 + settings = params[:settings]
  9 + settings ||= {}
  10 +
  11 + @settings = Noosfero::Plugin::Settings.new(environment, SiteTourPlugin, settings)
  12 + @settings.actions_csv = convert_to_csv(@settings.actions)
  13 + @settings.group_triggers_csv = convert_to_csv(@settings.group_triggers)
  14 +
  15 + if request.post?
  16 + @settings.actions = convert_actions_from_csv(settings[:actions_csv])
  17 + @settings.settings.delete(:actions_csv)
  18 +
  19 + @settings.group_triggers = convert_group_triggers_from_csv(settings[:group_triggers_csv])
  20 + @settings.settings.delete(:group_triggers_csv)
  21 +
  22 + @settings.save!
  23 + session[:notice] = 'Settings succefully saved.'
  24 + redirect_to :action => 'index'
  25 + end
  26 + end
  27 +
  28 + protected
  29 +
  30 + def convert_to_csv(actions)
  31 + CSV.generate do |csv|
  32 + (actions||[]).each { |action| csv << action.values }
  33 + end
  34 + end
  35 +
  36 + def convert_actions_from_csv(actions_csv)
  37 + return [] if actions_csv.blank?
  38 + CSV.parse(actions_csv).map do |action|
  39 + {:language => action[0], :group_name => action[1], :selector => action[2], :description => action[3]}
  40 + end
  41 + end
  42 +
  43 + def convert_group_triggers_from_csv(group_triggers_csv)
  44 + return [] if group_triggers_csv.blank?
  45 + CSV.parse(group_triggers_csv).map do |group|
  46 + {:group_name => group[0], :selector => group[1], :event => group[2]}
  47 + end
  48 + end
  49 +
  50 +end
plugins/site_tour/lib/ext/person.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +class Person
  2 +
  3 + settings_items :site_tour_plugin_actions, :type => Array, :default => []
  4 +
  5 +end
plugins/site_tour/lib/site_tour_plugin.rb 0 → 100644
@@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
  1 +class SiteTourPlugin < Noosfero::Plugin
  2 +
  3 + def self.plugin_name
  4 + 'SiteTourPlugin'
  5 + end
  6 +
  7 + def self.plugin_description
  8 + _("A site tour to show users how to use the application.")
  9 + end
  10 +
  11 + def stylesheet?
  12 + true
  13 + end
  14 +
  15 + def js_files
  16 + ['intro.min.js', 'main.js']
  17 + end
  18 +
  19 + def user_data_extras
  20 + proc do
  21 + logged_in? ? {:site_tour_plugin_actions => user.site_tour_plugin_actions}:{}
  22 + end
  23 + end
  24 +
  25 + def body_ending
  26 + proc do
  27 + tour_file = "/plugins/site_tour/tour/#{language}/tour.js"
  28 + js_file = File.exists?(Rails.root.join("public#{tour_file}").to_s) ? tour_file : ""
  29 + settings = Noosfero::Plugin::Settings.new(environment, SiteTourPlugin)
  30 + actions = (settings.actions||[]).select {|action| action[:language] == language}
  31 +
  32 + render(:file => 'tour_actions', :locals => { :actions => actions, :group_triggers => settings.group_triggers, :js_file => js_file})
  33 + end
  34 + end
  35 +
  36 + def self.extra_blocks
  37 + { SiteTourPlugin::TourBlock => {} }
  38 + end
  39 +
  40 + def self.actions_csv_default_setting
  41 + 'en,tour_plugin,.site-tour-plugin_tour-block .tour-button,"Click to start tour!"'
  42 + end
  43 +
  44 +end
plugins/site_tour/lib/site_tour_plugin/site_tour_helper.rb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +module SiteTourPlugin::SiteTourHelper
  2 +
  3 + def parse_tour_description(description)
  4 + p = profile rescue nil
  5 + if !p.nil? && description.present?
  6 + description.gsub('{profile.identifier}', p.identifier).
  7 + gsub('{profile.name}', p.name).
  8 + gsub('{profile.url}', url_for(p.url))
  9 + else
  10 + description
  11 + end
  12 + end
  13 +
  14 +end
plugins/site_tour/lib/site_tour_plugin/tour_block.rb 0 → 100644
@@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
  1 +class SiteTourPlugin::TourBlock < Block
  2 +
  3 + settings_items :actions, :type => Array, :default => [{:group_name => 'tour_plugin', :selector => '.site-tour-plugin_tour-block .tour-button', :description => _('Click to start tour!')}]
  4 + settings_items :group_triggers, :type => Array, :default => []
  5 + settings_items :display_button, :type => :boolean, :default => true
  6 +
  7 + attr_accessible :actions, :display_button, :group_triggers
  8 +
  9 + before_save do |block|
  10 + block.actions.reject! {|i| i[:group_name].blank? && i[:selector].blank? && i[:description].blank?}
  11 + block.group_triggers.reject! {|i| i[:group_name].blank? && i[:selector].blank?}
  12 + end
  13 +
  14 + def self.description
  15 + _('Site Tour Block')
  16 + end
  17 +
  18 + def help
  19 + _('Configure a step-by-step tour.')
  20 + end
  21 +
  22 + def content(args={})
  23 + block = self
  24 + proc do
  25 + render :file => 'blocks/tour', :locals => {:block => block}
  26 + end
  27 + end
  28 +
  29 +end
plugins/site_tour/po/pt/site_tour.po 0 → 100644
@@ -0,0 +1,119 @@ @@ -0,0 +1,119 @@
  1 +msgid ""
  2 +msgstr ""
  3 +"Project-Id-Version: 1.2~rc1-2843-g999a037\n"
  4 +"POT-Creation-Date: 2015-08-06 08:53-0300\n"
  5 +"PO-Revision-Date: 2015-02-03 17:27-0300\n"
  6 +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
  7 +"Language-Team: LANGUAGE <LL@li.org>\n"
  8 +"Language: \n"
  9 +"MIME-Version: 1.0\n"
  10 +"Content-Type: text/plain; charset=UTF-8\n"
  11 +"Content-Transfer-Encoding: 8bit\n"
  12 +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
  13 +
  14 +#: plugins/site_tour/lib/site_tour_plugin.rb:8
  15 +msgid "A site tour to show users how to use the application."
  16 +msgstr "Um plugin para apresentar aos usuários um tour da aplicação"
  17 +
  18 +#: plugins/site_tour/lib/site_tour_plugin/tour_block.rb:3
  19 +msgid "Click to start tour!"
  20 +msgstr "Clique para iniciar o tour!"
  21 +
  22 +#: plugins/site_tour/lib/site_tour_plugin/tour_block.rb:15
  23 +msgid "Site Tour Block"
  24 +msgstr "Bloco para Site Tour"
  25 +
  26 +#: plugins/site_tour/lib/site_tour_plugin/tour_block.rb:19
  27 +msgid "Configure a step-by-step tour."
  28 +msgstr "Configure o passo a passo do tour."
  29 +
  30 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block_item.html.erb:13
  31 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block_group_item.html.erb:15
  32 +msgid "Delete"
  33 +msgstr "Apagar"
  34 +
  35 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:4
  36 +msgid "Display help button"
  37 +msgstr "Mostrar o botão de ajuda"
  38 +
  39 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:8
  40 +msgid "Tooltip Actions"
  41 +msgstr "Ações do Tooltip"
  42 +
  43 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:9
  44 +msgid ""
  45 +"Special fields for description: {profile.name}, {profile.identifier}, "
  46 +"{profile.url}."
  47 +msgstr ""
  48 +"Campos especiais para a descrição: {profile.name}, {profile.identifier}, "
  49 +"{profile.url}"
  50 +
  51 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:11
  52 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:29
  53 +msgid "Group Name"
  54 +msgstr "Nome do Grupo"
  55 +
  56 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:12
  57 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:30
  58 +msgid "Selector"
  59 +msgstr "Seletor"
  60 +
  61 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:13
  62 +msgid "Description"
  63 +msgstr "Descrição"
  64 +
  65 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:23
  66 +msgid "New Tooltip"
  67 +msgstr "Novo Tooltip"
  68 +
  69 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:27
  70 +msgid "Group Triggers"
  71 +msgstr "Gatilhos dos Grupos"
  72 +
  73 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:31
  74 +msgid "Event"
  75 +msgstr "Evento"
  76 +
  77 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:41
  78 +msgid "New Group Trigger"
  79 +msgstr "Novo Gatilho de Grupo"
  80 +
  81 +#: plugins/site_tour/views/blocks/tour.html.erb:4
  82 +msgid "Help"
  83 +msgstr "Ajuda"
  84 +
  85 +#: plugins/site_tour/views/tour_actions.html.erb:16
  86 +msgid "Next"
  87 +msgstr "Próximo"
  88 +
  89 +#: plugins/site_tour/views/tour_actions.html.erb:17
  90 +msgid "Back"
  91 +msgstr "Anterior"
  92 +
  93 +#: plugins/site_tour/views/tour_actions.html.erb:18
  94 +msgid "Skip"
  95 +msgstr "Pular"
  96 +
  97 +#: plugins/site_tour/views/tour_actions.html.erb:19
  98 +msgid "Finish"
  99 +msgstr "Fim"
  100 +
  101 +#: plugins/site_tour/views/site_tour_plugin_admin/index.html.erb:1
  102 +msgid "Site Tour Settings"
  103 +msgstr "Configurações do Site Tour"
  104 +
  105 +#: plugins/site_tour/views/site_tour_plugin_admin/index.html.erb:5
  106 +msgid "Tooltips (CSV format: language, group name, selector, description)"
  107 +msgstr "Tooltips (Formato CSV: idioma, nome do grupo, seletor, descrição)"
  108 +
  109 +#: plugins/site_tour/views/site_tour_plugin_admin/index.html.erb:6
  110 +msgid ""
  111 +"Group Triggers (CSV format: group name, selector, event (e.g. mouseenter, "
  112 +"click))"
  113 +msgstr ""
  114 +"Gatilhos dos grupos (Formato CSV: nome do grupo, seletor, evento (e.g. "
  115 +"mouseenter, click))"
  116 +
  117 +#: plugins/site_tour/views/site_tour_plugin_admin/index.html.erb:9
  118 +msgid "Save"
  119 +msgstr "Salvar"
plugins/site_tour/po/site_tour.pot 0 → 100644
@@ -0,0 +1,121 @@ @@ -0,0 +1,121 @@
  1 +# SOME DESCRIPTIVE TITLE.
  2 +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
  3 +# This file is distributed under the same license as the PACKAGE package.
  4 +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
  5 +#
  6 +#, fuzzy
  7 +msgid ""
  8 +msgstr ""
  9 +"Project-Id-Version: 1.2~rc1-2843-g999a037\n"
  10 +"POT-Creation-Date: 2015-08-06 08:53-0300\n"
  11 +"PO-Revision-Date: 2015-02-03 17:27-0300\n"
  12 +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
  13 +"Language-Team: LANGUAGE <LL@li.org>\n"
  14 +"Language: \n"
  15 +"MIME-Version: 1.0\n"
  16 +"Content-Type: text/plain; charset=UTF-8\n"
  17 +"Content-Transfer-Encoding: 8bit\n"
  18 +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
  19 +
  20 +#: plugins/site_tour/lib/site_tour_plugin.rb:8
  21 +msgid "A site tour to show users how to use the application."
  22 +msgstr ""
  23 +
  24 +#: plugins/site_tour/lib/site_tour_plugin/tour_block.rb:3
  25 +msgid "Click to start tour!"
  26 +msgstr ""
  27 +
  28 +#: plugins/site_tour/lib/site_tour_plugin/tour_block.rb:15
  29 +msgid "Site Tour Block"
  30 +msgstr ""
  31 +
  32 +#: plugins/site_tour/lib/site_tour_plugin/tour_block.rb:19
  33 +msgid "Configure a step-by-step tour."
  34 +msgstr ""
  35 +
  36 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block_item.html.erb:13
  37 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block_group_item.html.erb:15
  38 +msgid "Delete"
  39 +msgstr ""
  40 +
  41 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:4
  42 +msgid "Display help button"
  43 +msgstr ""
  44 +
  45 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:8
  46 +msgid "Tooltip Actions"
  47 +msgstr ""
  48 +
  49 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:9
  50 +msgid ""
  51 +"Special fields for description: {profile.name}, {profile.identifier}, "
  52 +"{profile.url}."
  53 +msgstr ""
  54 +
  55 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:11
  56 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:29
  57 +msgid "Group Name"
  58 +msgstr ""
  59 +
  60 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:12
  61 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:30
  62 +msgid "Selector"
  63 +msgstr ""
  64 +
  65 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:13
  66 +msgid "Description"
  67 +msgstr ""
  68 +
  69 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:23
  70 +msgid "New Tooltip"
  71 +msgstr ""
  72 +
  73 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:27
  74 +msgid "Group Triggers"
  75 +msgstr ""
  76 +
  77 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:31
  78 +msgid "Event"
  79 +msgstr ""
  80 +
  81 +#: plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb:41
  82 +msgid "New Group Trigger"
  83 +msgstr ""
  84 +
  85 +#: plugins/site_tour/views/blocks/tour.html.erb:4
  86 +msgid "Help"
  87 +msgstr ""
  88 +
  89 +#: plugins/site_tour/views/tour_actions.html.erb:16
  90 +msgid "Next"
  91 +msgstr ""
  92 +
  93 +#: plugins/site_tour/views/tour_actions.html.erb:17
  94 +msgid "Back"
  95 +msgstr ""
  96 +
  97 +#: plugins/site_tour/views/tour_actions.html.erb:18
  98 +msgid "Skip"
  99 +msgstr ""
  100 +
  101 +#: plugins/site_tour/views/tour_actions.html.erb:19
  102 +msgid "Finish"
  103 +msgstr ""
  104 +
  105 +#: plugins/site_tour/views/site_tour_plugin_admin/index.html.erb:1
  106 +msgid "Site Tour Settings"
  107 +msgstr ""
  108 +
  109 +#: plugins/site_tour/views/site_tour_plugin_admin/index.html.erb:5
  110 +msgid "Tooltips (CSV format: language, group name, selector, description)"
  111 +msgstr ""
  112 +
  113 +#: plugins/site_tour/views/site_tour_plugin_admin/index.html.erb:6
  114 +msgid ""
  115 +"Group Triggers (CSV format: group name, selector, event (e.g. mouseenter, "
  116 +"click))"
  117 +msgstr ""
  118 +
  119 +#: plugins/site_tour/views/site_tour_plugin_admin/index.html.erb:9
  120 +msgid "Save"
  121 +msgstr ""
plugins/site_tour/public/edit_tour_block.css 0 → 100644
@@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
  1 +#edit-tour-block #tooltip-actions h3 {
  2 + margin-bottom: 5px;
  3 +}
  4 +
  5 +#edit-tour-block .special-attributes {
  6 + color: rgb(157, 157, 157);
  7 +}
  8 +
  9 +#edit-tour-block .list-items {
  10 + margin-bottom: 25px;
  11 +}
  12 +
  13 +#edit-tour-block .droppable-items {
  14 + padding-left: 0;
  15 + margin-top: -12px;
  16 +}
  17 +
  18 +#edit-tour-block .droppable-items li {
  19 + list-style-type: none;
  20 +}
  21 +
  22 +#edit-tour-block .item-row {
  23 + line-height: 25px;
  24 + margin-bottom: 5px;
  25 + padding: 0;
  26 + cursor: pointer;
  27 + width: 97%;
  28 +}
  29 +
  30 +#edit-tour-block .item-row:hover {
  31 + background: #ddd url(/images/drag-and-drop.png) no-repeat;
  32 + background-position: 98% 15px;
  33 +}
  34 +
  35 +#edit-tour-block .item-row li {
  36 + list-style-type: none;
  37 + display: inline;
  38 + margin-left: 5px;
  39 +}
  40 +
  41 +#edit-tour-block {
  42 + width: 620px;
  43 + position: relative;
  44 +}
  45 +
  46 +#edit-tour-block #new-template {
  47 + display: none;
  48 +}
  49 +
  50 +#edit-tour-block .list-header {
  51 + width: 98%;
  52 + padding: 0 1px 10px 10px;
  53 + margin-bottom: 5px;
  54 + cursor: pointer;
  55 +}
  56 +
  57 +#edit-tour-block .list-header li {
  58 + list-style-type: none;
  59 + display: inline;
  60 + font-weight: bold;
  61 + font-size: 12px;
  62 + text-align: center;
  63 +}
  64 +
  65 +#edit-tour-block .list-header .list-name {
  66 + margin-left: 20px;
  67 +}
  68 +#edit-tour-block .list-header .list-selector {
  69 + margin-left: 63px;
  70 +}
  71 +#edit-tour-block .list-header .list-description, #edit-tour-block .list-header .list-event {
  72 + margin-left: 68px;
  73 +}
plugins/site_tour/public/edit_tour_block.js 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +jQuery(document).ready(function(){
  2 + jQuery('#edit-tour-block').on('click', '.add-item', function() {
  3 + var container = jQuery(this).closest('.list-items');
  4 + var new_action = container.find('#new-template>li').clone();
  5 + new_action.show();
  6 + container.find('.droppable-items').append(new_action);
  7 + });
  8 +
  9 + jQuery('#edit-tour-block').on('click', '.delete-tour-block-item', function() {
  10 + jQuery(this).parent().parent().remove();
  11 + return false;
  12 + });
  13 +
  14 + jQuery("#edit-tour-block .droppable-items").sortable({
  15 + revert: true,
  16 + axis: "y"
  17 + });
  18 +});
plugins/site_tour/public/intro.min.js 0 → 100644
@@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
  1 +(function(w,p){"object"===typeof exports?p(exports):"function"===typeof define&&define.amd?define(["exports"],p):p(w)})(this,function(w){function p(a){this._targetElement=a;this._options={nextLabel:"Next &rarr;",prevLabel:"&larr; Back",skipLabel:"Skip",doneLabel:"Done",tooltipPosition:"bottom",tooltipClass:"",highlightClass:"",exitOnEsc:!0,exitOnOverlayClick:!0,showStepNumbers:!0,keyboardNavigation:!0,showButtons:!0,showBullets:!0,showProgress:!1,scrollToElement:!0,overlayOpacity:0.8,positionPrecedence:["bottom",
  2 +"top","right","left"],disableInteraction:!1}}function J(a){var b=[],c=this;if(this._options.steps)for(var d=[],e=0,d=this._options.steps.length;e<d;e++){var f=A(this._options.steps[e]);f.step=b.length+1;"string"===typeof f.element&&(f.element=document.querySelector(f.element));if("undefined"===typeof f.element||null==f.element){var h=document.querySelector(".introjsFloatingElement");null==h&&(h=document.createElement("div"),h.className="introjsFloatingElement",document.body.appendChild(h));f.element=
  3 +h;f.position="floating"}null!=f.element&&b.push(f)}else{d=a.querySelectorAll("*[data-intro]");if(1>d.length)return!1;e=0;for(f=d.length;e<f;e++){var h=d[e],k=parseInt(h.getAttribute("data-step"),10);0<k&&(b[k-1]={element:h,intro:h.getAttribute("data-intro"),step:parseInt(h.getAttribute("data-step"),10),tooltipClass:h.getAttribute("data-tooltipClass"),highlightClass:h.getAttribute("data-highlightClass"),position:h.getAttribute("data-position")||this._options.tooltipPosition})}e=k=0;for(f=d.length;e<
  4 +f;e++)if(h=d[e],null==h.getAttribute("data-step")){for(;"undefined"!=typeof b[k];)k++;b[k]={element:h,intro:h.getAttribute("data-intro"),step:k+1,tooltipClass:h.getAttribute("data-tooltipClass"),highlightClass:h.getAttribute("data-highlightClass"),position:h.getAttribute("data-position")||this._options.tooltipPosition}}}e=[];for(d=0;d<b.length;d++)b[d]&&e.push(b[d]);b=e;b.sort(function(a,b){return a.step-b.step});c._introItems=b;K.call(c,a)&&(x.call(c),a.querySelector(".introjs-skipbutton"),a.querySelector(".introjs-nextbutton"),
  5 +c._onKeyDown=function(b){if(27===b.keyCode&&!0==c._options.exitOnEsc)y.call(c,a),void 0!=c._introExitCallback&&c._introExitCallback.call(c);else if(37===b.keyCode)C.call(c);else if(39===b.keyCode)x.call(c);else if(13===b.keyCode){var d=b.target||b.srcElement;d&&0<d.className.indexOf("introjs-prevbutton")?C.call(c):d&&0<d.className.indexOf("introjs-skipbutton")?y.call(c,a):x.call(c);b.preventDefault?b.preventDefault():b.returnValue=!1}},c._onResize=function(a){t.call(c,document.querySelector(".introjs-helperLayer"));
  6 +t.call(c,document.querySelector(".introjs-tooltipReferenceLayer"))},window.addEventListener?(this._options.keyboardNavigation&&window.addEventListener("keydown",c._onKeyDown,!0),window.addEventListener("resize",c._onResize,!0)):document.attachEvent&&(this._options.keyboardNavigation&&document.attachEvent("onkeydown",c._onKeyDown),document.attachEvent("onresize",c._onResize)));return!1}function A(a){if(null==a||"object"!=typeof a||"undefined"!=typeof a.nodeType)return a;var b={},c;for(c in a)b[c]=
  7 +A(a[c]);return b}function x(){this._direction="forward";"undefined"===typeof this._currentStep?this._currentStep=0:++this._currentStep;if(this._introItems.length<=this._currentStep)"function"===typeof this._introCompleteCallback&&this._introCompleteCallback.call(this),y.call(this,this._targetElement);else{var a=this._introItems[this._currentStep];"undefined"!==typeof this._introBeforeChangeCallback&&this._introBeforeChangeCallback.call(this,a.element);G.call(this,a)}}function C(){this._direction=
  8 +"backward";if(0===this._currentStep)return!1;var a=this._introItems[--this._currentStep];"undefined"!==typeof this._introBeforeChangeCallback&&this._introBeforeChangeCallback.call(this,a.element);G.call(this,a)}function y(a){var b=a.querySelector(".introjs-overlay");if(null!=b){b.style.opacity=0;setTimeout(function(){b.parentNode&&b.parentNode.removeChild(b)},500);var c=a.querySelector(".introjs-helperLayer");c&&c.parentNode.removeChild(c);(c=a.querySelector(".introjs-tooltipReferenceLayer"))&&c.parentNode.removeChild(c);
  9 +(a=a.querySelector(".introjs-disableInteraction"))&&a.parentNode.removeChild(a);(a=document.querySelector(".introjsFloatingElement"))&&a.parentNode.removeChild(a);if(a=document.querySelector(".introjs-showElement"))a.className=a.className.replace(/introjs-[a-zA-Z]+/g,"").replace(/^\s+|\s+$/g,"");if((a=document.querySelectorAll(".introjs-fixParent"))&&0<a.length)for(c=a.length-1;0<=c;c--)a[c].className=a[c].className.replace(/introjs-fixParent/g,"").replace(/^\s+|\s+$/g,"");window.removeEventListener?
  10 +window.removeEventListener("keydown",this._onKeyDown,!0):document.detachEvent&&document.detachEvent("onkeydown",this._onKeyDown);this._currentStep=void 0}}function H(a,b,c,d){var e="";b.style.top=null;b.style.right=null;b.style.bottom=null;b.style.left=null;b.style.marginLeft=null;b.style.marginTop=null;c.style.display="inherit";"undefined"!=typeof d&&null!=d&&(d.style.top=null,d.style.left=null);if(this._introItems[this._currentStep]){e=this._introItems[this._currentStep];e="string"===typeof e.tooltipClass?
  11 +e.tooltipClass:this._options.tooltipClass;b.className=("introjs-tooltip "+e).replace(/^\s+|\s+$/g,"");currentTooltipPosition=this._introItems[this._currentStep].position;if(("auto"==currentTooltipPosition||"auto"==this._options.tooltipPosition)&&"floating"!=currentTooltipPosition){var e=currentTooltipPosition,f=this._options.positionPrecedence.slice(),h=F(),p=k(b).height+10,s=k(b).width+20,l=k(a),m="floating";l.left+s>h.width||0>l.left+l.width/2-s?(q(f,"bottom"),q(f,"top")):(l.height+l.top+p>h.height&&
  12 +q(f,"bottom"),0>l.top-p&&q(f,"top"));l.width+l.left+s>h.width&&q(f,"right");0>l.left-s&&q(f,"left");0<f.length&&(m=f[0]);e&&"auto"!=e&&-1<f.indexOf(e)&&(m=e);currentTooltipPosition=m}e=k(a);f=k(b).height;h=F();switch(currentTooltipPosition){case "top":b.style.left="15px";b.style.top="-"+(f+10)+"px";c.className="introjs-arrow bottom";break;case "right":b.style.left=k(a).width+20+"px";e.top+f>h.height&&(c.className="introjs-arrow left-bottom",b.style.top="-"+(f-e.height-20)+"px");c.className="introjs-arrow left";
  13 +break;case "left":!0==this._options.showStepNumbers&&(b.style.top="15px");e.top+f>h.height?(b.style.top="-"+(f-e.height-20)+"px",c.className="introjs-arrow right-bottom"):c.className="introjs-arrow right";b.style.right=e.width+20+"px";break;case "floating":c.style.display="none";a=k(b);b.style.left="50%";b.style.top="50%";b.style.marginLeft="-"+a.width/2+"px";b.style.marginTop="-"+a.height/2+"px";"undefined"!=typeof d&&null!=d&&(d.style.left="-"+(a.width/2+18)+"px",d.style.top="-"+(a.height/2+18)+
  14 +"px");break;case "bottom-right-aligned":c.className="introjs-arrow top-right";b.style.right="0px";b.style.bottom="-"+(k(b).height+10)+"px";break;case "bottom-middle-aligned":d=k(a);a=k(b);c.className="introjs-arrow top-middle";b.style.left=d.width/2-a.width/2+"px";b.style.bottom="-"+(a.height+10)+"px";break;default:b.style.bottom="-"+(k(b).height+10)+"px",b.style.left=k(a).width/2-k(b).width/2+"px",c.className="introjs-arrow top"}}}function q(a,b){-1<a.indexOf(b)&&a.splice(a.indexOf(b),1)}function t(a){if(a&&
  15 +this._introItems[this._currentStep]){var b=this._introItems[this._currentStep],c=k(b.element),d=10;"floating"==b.position&&(d=0);a.setAttribute("style","width: "+(c.width+d)+"px; height:"+(c.height+d)+"px; top:"+(c.top-5)+"px;left: "+(c.left-5)+"px;")}}function L(){var a=document.querySelector(".introjs-disableInteraction");null===a&&(a=document.createElement("div"),a.className="introjs-disableInteraction",this._targetElement.appendChild(a));t.call(this,a)}function G(a){"undefined"!==typeof this._introChangeCallback&&
  16 +this._introChangeCallback.call(this,a.element);var b=this,c=document.querySelector(".introjs-helperLayer"),d=document.querySelector(".introjs-tooltipReferenceLayer"),e="introjs-helperLayer";k(a.element);"string"===typeof a.highlightClass&&(e+=" "+a.highlightClass);"string"===typeof this._options.highlightClass&&(e+=" "+this._options.highlightClass);if(null!=c){var f=d.querySelector(".introjs-helperNumberLayer"),h=d.querySelector(".introjs-tooltiptext"),p=d.querySelector(".introjs-arrow"),s=d.querySelector(".introjs-tooltip"),
  17 +l=d.querySelector(".introjs-skipbutton"),m=d.querySelector(".introjs-prevbutton"),r=d.querySelector(".introjs-nextbutton");c.className=e;s.style.opacity=0;s.style.display="none";if(null!=f){var g=this._introItems[0<=a.step-2?a.step-2:0];if(null!=g&&"forward"==this._direction&&"floating"==g.position||"backward"==this._direction&&"floating"==a.position)f.style.opacity=0}t.call(b,c);t.call(b,d);if((g=document.querySelectorAll(".introjs-fixParent"))&&0<g.length)for(e=g.length-1;0<=e;e--)g[e].className=
  18 +g[e].className.replace(/introjs-fixParent/g,"").replace(/^\s+|\s+$/g,"");g=document.querySelector(".introjs-showElement");g.className=g.className.replace(/introjs-[a-zA-Z]+/g,"").replace(/^\s+|\s+$/g,"");b._lastShowElementTimer&&clearTimeout(b._lastShowElementTimer);b._lastShowElementTimer=setTimeout(function(){null!=f&&(f.innerHTML=a.step);h.innerHTML=a.intro;s.style.display="block";H.call(b,a.element,s,p,f);d.querySelector(".introjs-bullets li > a.active").className="";d.querySelector('.introjs-bullets li > a[data-stepnumber="'+
  19 +a.step+'"]').className="active";d.querySelector(".introjs-progress .introjs-progressbar").setAttribute("style","width:"+I.call(b)+"%;");s.style.opacity=1;f&&(f.style.opacity=1);-1===r.tabIndex?l.focus():r.focus()},350)}else{var q=document.createElement("div"),m=document.createElement("div"),c=document.createElement("div"),n=document.createElement("div"),w=document.createElement("div"),D=document.createElement("div"),E=document.createElement("div"),u=document.createElement("div");q.className=e;m.className=
  20 +"introjs-tooltipReferenceLayer";t.call(b,q);t.call(b,m);this._targetElement.appendChild(q);this._targetElement.appendChild(m);c.className="introjs-arrow";w.className="introjs-tooltiptext";w.innerHTML=a.intro;D.className="introjs-bullets";!1===this._options.showBullets&&(D.style.display="none");for(var q=document.createElement("ul"),e=0,B=this._introItems.length;e<B;e++){var A=document.createElement("li"),z=document.createElement("a");z.onclick=function(){b.goToStep(this.getAttribute("data-stepnumber"))};
  21 +e===a.step-1&&(z.className="active");z.href="javascript:void(0);";z.innerHTML="&nbsp;";z.setAttribute("data-stepnumber",this._introItems[e].step);A.appendChild(z);q.appendChild(A)}D.appendChild(q);E.className="introjs-progress";!1===this._options.showProgress&&(E.style.display="none");e=document.createElement("div");e.className="introjs-progressbar";e.setAttribute("style","width:"+I.call(this)+"%;");E.appendChild(e);u.className="introjs-tooltipbuttons";!1===this._options.showButtons&&(u.style.display=
  22 +"none");n.className="introjs-tooltip";n.appendChild(w);n.appendChild(D);n.appendChild(E);!0==this._options.showStepNumbers&&(g=document.createElement("span"),g.className="introjs-helperNumberLayer",g.innerHTML=a.step,m.appendChild(g));n.appendChild(c);m.appendChild(n);r=document.createElement("a");r.onclick=function(){b._introItems.length-1!=b._currentStep&&x.call(b)};r.href="javascript:void(0);";r.innerHTML=this._options.nextLabel;m=document.createElement("a");m.onclick=function(){0!=b._currentStep&&
  23 +C.call(b)};m.href="javascript:void(0);";m.innerHTML=this._options.prevLabel;l=document.createElement("a");l.className="introjs-button introjs-skipbutton";l.href="javascript:void(0);";l.innerHTML=this._options.skipLabel;l.onclick=function(){b._introItems.length-1==b._currentStep&&"function"===typeof b._introCompleteCallback&&b._introCompleteCallback.call(b);b._introItems.length-1!=b._currentStep&&"function"===typeof b._introExitCallback&&b._introExitCallback.call(b);y.call(b,b._targetElement)};u.appendChild(l);
  24 +1<this._introItems.length&&(u.appendChild(m),u.appendChild(r));n.appendChild(u);H.call(b,a.element,n,c,g)}!0===this._options.disableInteraction&&L.call(b);m.removeAttribute("tabIndex");r.removeAttribute("tabIndex");0==this._currentStep&&1<this._introItems.length?(m.className="introjs-button introjs-prevbutton introjs-disabled",m.tabIndex="-1",r.className="introjs-button introjs-nextbutton",l.innerHTML=this._options.skipLabel):this._introItems.length-1==this._currentStep||1==this._introItems.length?
  25 +(l.innerHTML=this._options.doneLabel,m.className="introjs-button introjs-prevbutton",r.className="introjs-button introjs-nextbutton introjs-disabled",r.tabIndex="-1"):(m.className="introjs-button introjs-prevbutton",r.className="introjs-button introjs-nextbutton",l.innerHTML=this._options.skipLabel);r.focus();a.element.className+=" introjs-showElement";g=v(a.element,"position");"absolute"!==g&&"relative"!==g&&(a.element.className+=" introjs-relativePosition");for(g=a.element.parentNode;null!=g&&"body"!==
  26 +g.tagName.toLowerCase();){c=v(g,"z-index");n=parseFloat(v(g,"opacity"));u=v(g,"transform")||v(g,"-webkit-transform")||v(g,"-moz-transform")||v(g,"-ms-transform")||v(g,"-o-transform");if(/[0-9]+/.test(c)||1>n||"none"!==u)g.className+=" introjs-fixParent";g=g.parentNode}M(a.element)||!0!==this._options.scrollToElement||(n=a.element.getBoundingClientRect(),g=F().height,c=n.bottom-(n.bottom-n.top),n=n.bottom-g,0>c||a.element.clientHeight>g?window.scrollBy(0,c-30):window.scrollBy(0,n+100));"undefined"!==
  27 +typeof this._introAfterChangeCallback&&this._introAfterChangeCallback.call(this,a.element)}function v(a,b){var c="";a.currentStyle?c=a.currentStyle[b]:document.defaultView&&document.defaultView.getComputedStyle&&(c=document.defaultView.getComputedStyle(a,null).getPropertyValue(b));return c&&c.toLowerCase?c.toLowerCase():c}function F(){if(void 0!=window.innerWidth)return{width:window.innerWidth,height:window.innerHeight};var a=document.documentElement;return{width:a.clientWidth,height:a.clientHeight}}
  28 +function M(a){a=a.getBoundingClientRect();return 0<=a.top&&0<=a.left&&a.bottom+80<=window.innerHeight&&a.right<=window.innerWidth}function K(a){var b=document.createElement("div"),c="",d=this;b.className="introjs-overlay";if("body"===a.tagName.toLowerCase())c+="top: 0;bottom: 0; left: 0;right: 0;position: fixed;",b.setAttribute("style",c);else{var e=k(a);e&&(c+="width: "+e.width+"px; height:"+e.height+"px; top:"+e.top+"px;left: "+e.left+"px;",b.setAttribute("style",c))}a.appendChild(b);b.onclick=
  29 +function(){!0==d._options.exitOnOverlayClick&&(y.call(d,a),void 0!=d._introExitCallback&&d._introExitCallback.call(d))};setTimeout(function(){c+="opacity: "+d._options.overlayOpacity.toString()+";";b.setAttribute("style",c)},10);return!0}function k(a){var b={};b.width=a.offsetWidth;b.height=a.offsetHeight;for(var c=0,d=0;a&&!isNaN(a.offsetLeft)&&!isNaN(a.offsetTop);)c+=a.offsetLeft,d+=a.offsetTop,a=a.offsetParent;b.top=d;b.left=c;return b}function I(){return 100*(parseInt(this._currentStep+1,10)/
  30 +this._introItems.length)}var B=function(a){if("object"===typeof a)return new p(a);if("string"===typeof a){if(a=document.querySelector(a))return new p(a);throw Error("There is no element with given selector.");}return new p(document.body)};B.version="1.0.0";B.fn=p.prototype={clone:function(){return new p(this)},setOption:function(a,b){this._options[a]=b;return this},setOptions:function(a){var b=this._options,c={},d;for(d in b)c[d]=b[d];for(d in a)c[d]=a[d];this._options=c;return this},start:function(){J.call(this,
  31 +this._targetElement);return this},goToStep:function(a){this._currentStep=a-2;"undefined"!==typeof this._introItems&&x.call(this);return this},nextStep:function(){x.call(this);return this},previousStep:function(){C.call(this);return this},exit:function(){y.call(this,this._targetElement);return this},refresh:function(){t.call(this,document.querySelector(".introjs-helperLayer"));t.call(this,document.querySelector(".introjs-tooltipReferenceLayer"));return this},onbeforechange:function(a){if("function"===
  32 +typeof a)this._introBeforeChangeCallback=a;else throw Error("Provided callback for onbeforechange was not a function");return this},onchange:function(a){if("function"===typeof a)this._introChangeCallback=a;else throw Error("Provided callback for onchange was not a function.");return this},onafterchange:function(a){if("function"===typeof a)this._introAfterChangeCallback=a;else throw Error("Provided callback for onafterchange was not a function");return this},oncomplete:function(a){if("function"===
  33 +typeof a)this._introCompleteCallback=a;else throw Error("Provided callback for oncomplete was not a function.");return this},onexit:function(a){if("function"===typeof a)this._introExitCallback=a;else throw Error("Provided callback for onexit was not a function.");return this}};return w.introJs=B});
plugins/site_tour/public/introjs.min.css 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +.introjs-overlay{position:absolute;z-index:999999;background-color:#000;opacity:0;background:-moz-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:-webkit-gradient(radial,center center,0px,center center,100%,color-stop(0%,rgba(0,0,0,0.4)),color-stop(100%,rgba(0,0,0,0.9)));background:-webkit-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:-o-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:-ms-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#66000000',endColorstr='#e6000000',GradientType=1);-ms-filter:"alpha(opacity=50)";filter:alpha(opacity=50);-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-fixParent{z-index:auto!important;opacity:1.0!important;position:absolute!important;-webkit-transform:none!important;-moz-transform:none!important;-ms-transform:none!important;-o-transform:none!important;transform:none!important}.introjs-showElement,tr.introjs-showElement>td,tr.introjs-showElement>th{z-index:9999999!important}.introjs-disableInteraction{z-index:99999999!important;position:absolute}.introjs-relativePosition,tr.introjs-showElement>td,tr.introjs-showElement>th{position:relative}.introjs-helperLayer{position:absolute;z-index:9999998;background-color:#FFF;background-color:rgba(255,255,255,.9);border:1px solid #777;border:1px solid rgba(0,0,0,.5);border-radius:4px;box-shadow:0 2px 15px rgba(0,0,0,.4);-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-tooltipReferenceLayer{position:absolute;z-index:10000000;background-color:transparent;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-helperLayer *,.introjs-helperLayer *:before,.introjs-helperLayer *:after{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;-ms-box-sizing:content-box;-o-box-sizing:content-box;box-sizing:content-box}.introjs-helperNumberLayer{position:absolute;top:-16px;left:-16px;z-index:9999999999!important;padding:2px;font-family:Arial,verdana,tahoma;font-size:13px;font-weight:bold;color:white;text-align:center;text-shadow:1px 1px 1px rgba(0,0,0,.3);background:#ff3019;background:-webkit-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ff3019),color-stop(100%,#cf0404));background:-moz-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-ms-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-o-linear-gradient(top,#ff3019 0,#cf0404 100%);background:linear-gradient(to bottom,#ff3019 0,#cf0404 100%);width:20px;height:20px;line-height:20px;border:3px solid white;border-radius:50%;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3019',endColorstr='#cf0404',GradientType=0);filter:progid:DXImageTransform.Microsoft.Shadow(direction=135,strength=2,color=ff0000);box-shadow:0 2px 5px rgba(0,0,0,.4)}.introjs-arrow{border:5px solid white;content:'';position:absolute}.introjs-arrow.top{top:-10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.top-right{top:-10px;right:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.top-middle{top:-10px;left:50%;margin-left:-5px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.right{right:-10px;top:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:white}.introjs-arrow.right-bottom{bottom:10px;right:-10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:white}.introjs-arrow.bottom{bottom:-10px;border-top-color:white;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.introjs-arrow.left{left:-10px;top:10px;border-top-color:transparent;border-right-color:white;border-bottom-color:transparent;border-left-color:transparent}.introjs-arrow.left-bottom{left:-10px;bottom:10px;border-top-color:transparent;border-right-color:white;border-bottom-color:transparent;border-left-color:transparent}.introjs-tooltip{position:absolute;padding:10px;background-color:white;min-width:200px;max-width:300px;border-radius:3px;box-shadow:0 1px 10px rgba(0,0,0,.4);-webkit-transition:opacity .1s ease-out;-moz-transition:opacity .1s ease-out;-ms-transition:opacity .1s ease-out;-o-transition:opacity .1s ease-out;transition:opacity .1s ease-out}.introjs-tooltipbuttons{text-align:right;white-space:nowrap}.introjs-button{position:relative;overflow:visible;display:inline-block;padding:.3em .8em;border:1px solid #d4d4d4;margin:0;text-decoration:none;text-shadow:1px 1px 0 #fff;font:11px/normal sans-serif;color:#333;white-space:nowrap;cursor:pointer;outline:0;background-color:#ececec;background-image:-webkit-gradient(linear,0 0,0 100%,from(#f4f4f4),to(#ececec));background-image:-moz-linear-gradient(#f4f4f4,#ececec);background-image:-o-linear-gradient(#f4f4f4,#ececec);background-image:linear-gradient(#f4f4f4,#ececec);-webkit-background-clip:padding;-moz-background-clip:padding;-o-background-clip:padding-box;-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;zoom:1;*display:inline;margin-top:10px}.introjs-button:hover{border-color:#bcbcbc;text-decoration:none;box-shadow:0 1px 1px #e3e3e3}.introjs-button:focus,.introjs-button:active{background-image:-webkit-gradient(linear,0 0,0 100%,from(#ececec),to(#f4f4f4));background-image:-moz-linear-gradient(#ececec,#f4f4f4);background-image:-o-linear-gradient(#ececec,#f4f4f4);background-image:linear-gradient(#ececec,#f4f4f4)}.introjs-button::-moz-focus-inner{padding:0;border:0}.introjs-skipbutton{margin-right:5px;color:#7a7a7a}.introjs-prevbutton{-webkit-border-radius:.2em 0 0 .2em;-moz-border-radius:.2em 0 0 .2em;border-radius:.2em 0 0 .2em;border-right:0}.introjs-nextbutton{-webkit-border-radius:0 .2em .2em 0;-moz-border-radius:0 .2em .2em 0;border-radius:0 .2em .2em 0}.introjs-disabled,.introjs-disabled:hover,.introjs-disabled:focus{color:#9a9a9a;border-color:#d4d4d4;box-shadow:none;cursor:default;background-color:#f4f4f4;background-image:none;text-decoration:none}.introjs-bullets{text-align:center}.introjs-bullets ul{clear:both;margin:15px auto 0;padding:0;display:inline-block}.introjs-bullets ul li{list-style:none;float:left;margin:0 2px}.introjs-bullets ul li a{display:block;width:6px;height:6px;background:#ccc;border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px;text-decoration:none}.introjs-bullets ul li a:hover{background:#999}.introjs-bullets ul li a.active{background:#999}.introjs-progress{overflow:hidden;height:10px;margin:10px 0 5px 0;border-radius:4px;background-color:#ecf0f1}.introjs-progressbar{float:left;width:0;height:100%;font-size:10px;line-height:10px;text-align:center;background-color:#08c}.introjsFloatingElement{position:absolute;height:0;width:0;left:50%;top:50%}
0 \ No newline at end of file 2 \ No newline at end of file
plugins/site_tour/public/main.js 0 → 100644
@@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
  1 +var siteTourPlugin = (function() {
  2 +
  3 + var actions = [];
  4 + var groupTriggers = [];
  5 + var userData = {};
  6 + var intro;
  7 + var options = {};
  8 +
  9 + function hasMark(name) {
  10 + return jQuery.cookie("_noosfero_.sitetour." + name) ||
  11 + jQuery.inArray(name, userData.site_tour_plugin_actions)>=0;
  12 + }
  13 +
  14 + function mark(name) {
  15 + jQuery.cookie("_noosfero_.sitetour." + name, 1, {expires: 365});
  16 + if(userData.login) {
  17 + jQuery.post('/plugin/site_tour/public/mark_action', {action_name: name}, function(data) { });
  18 + }
  19 + }
  20 +
  21 + function clearAll() {
  22 + jQuery('.site-tour-plugin').removeAttr('data-intro data-intro-name data-step');
  23 + }
  24 +
  25 + function configureIntro(force, actions) {
  26 + clearAll();
  27 + for(var i=0; i<actions.length; i++) {
  28 + var action = actions[i];
  29 +
  30 + if(force || !hasMark(action.name)) {
  31 + var el = jQuery(action.selector).filter(function() {
  32 + return jQuery(this).is(":visible") && jQuery(this).css('visibility') != 'hidden';
  33 + });
  34 + el.addClass('site-tour-plugin');
  35 + el.attr('data-intro', action.text);
  36 + el.attr('data-intro-name', action.name);
  37 + if(action.step) {
  38 + el.attr('data-step', action.step);
  39 + }
  40 + }
  41 + }
  42 + }
  43 +
  44 + function actionsOnload() {
  45 + var groups = jQuery.map(groupTriggers, function(g) { return g.name; });
  46 + return jQuery.grep(actions, function(n, i) { return jQuery.inArray(n.name, groups); });
  47 + }
  48 +
  49 + function actionsByGroup(group) {
  50 + return jQuery.grep(actions, function(n, i) { return n.name===group });
  51 + }
  52 +
  53 + function forceParam() {
  54 + return jQuery.deparam.querystring()['siteTourPlugin']==='force';
  55 + }
  56 +
  57 + return {
  58 + setOption: function(key, value) {
  59 + options[key] = value;
  60 + },
  61 + add: function (name, selector, text, step) {
  62 + actions.push({name: name, selector: selector, text: text, step: step});
  63 + },
  64 + addGroupTrigger: function(name, selector, ev) {
  65 + groupTriggers.push({name: name, selector: selector, event: ev});
  66 + plugin = this;
  67 + var handler = function() {
  68 + configureIntro(forceParam(), actionsByGroup(name));
  69 + intro.start();
  70 + jQuery(document).off(ev, selector, handler);
  71 + };
  72 + jQuery(document).on(ev, selector, handler);
  73 + },
  74 + start: function(data, force) {
  75 + force = typeof force !== 'undefined' ? force : false || forceParam();
  76 + userData = data;
  77 +
  78 + intro = introJs();
  79 + intro.setOption('tooltipPosition', 'auto');
  80 + intro.setOption('showStepNumbers', 'false');
  81 + intro.setOptions(options);
  82 + intro.onafterchange(function(targetElement) {
  83 + var name = jQuery(targetElement).attr('data-intro-name');
  84 + mark(name);
  85 + });
  86 + configureIntro(force, actionsOnload());
  87 + intro.start();
  88 + },
  89 + force: function() {
  90 + this.start({}, true);
  91 + }
  92 + }
  93 +})();
  94 +
  95 +jQuery( document ).ready(function( $ ) {
  96 + $(window).bind('userDataLoaded', function(event, data) {
  97 + siteTourPlugin.start(data);
  98 + });
  99 +});
plugins/site_tour/public/style.css 0 → 120000
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +introjs.min.css
0 \ No newline at end of file 2 \ No newline at end of file
plugins/site_tour/public/tour/en/tour.js.dist 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +jQuery( document ).ready(function( $ ) {
  2 + siteTourPlugin.add('login_button','.action-home-index #link_login', "Click to login!");
  3 + siteTourPlugin.add('login_button','.action-home-index .signup', "Click to signup!");
  4 + siteTourPlugin.add('logout_button','.action-home-index #logout', "Click to logout");
  5 + siteTourPlugin.add('navigation_bar','#navigation', "Click to navigate");
  6 +});
plugins/site_tour/test/functional/site_tour_plugin_admin_controller_test.rb 0 → 100644
@@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class SiteTourPluginAdminControllerTest < ActionController::TestCase
  4 +
  5 + def setup
  6 + @environment = Environment.default
  7 + login_as(create_admin_user(@environment))
  8 + end
  9 +
  10 + attr_reader :environment
  11 +
  12 + should 'parse csv and save actions array in plugin settings' do
  13 + actions_csv = "en,tour_plugin,.tour-button,Click"
  14 + post :index, :settings => {"actions_csv" => actions_csv}
  15 + @settings = Noosfero::Plugin::Settings.new(environment.reload, SiteTourPlugin)
  16 + assert_equal [{:language => 'en', :group_name => 'tour_plugin', :selector => '.tour-button', :description => 'Click'}], @settings.actions
  17 + end
  18 +
  19 + should 'parse csv and save group triggers array in plugin settings' do
  20 + group_triggers_csv = "tour_plugin,.tour-button,mouseenter"
  21 + post :index, :settings => {"group_triggers_csv" => group_triggers_csv}
  22 + @settings = Noosfero::Plugin::Settings.new(environment.reload, SiteTourPlugin)
  23 + assert_equal [{:group_name => 'tour_plugin', :selector => '.tour-button', :event => 'mouseenter'}], @settings.group_triggers
  24 + end
  25 +
  26 + should 'do not store actions_csv' do
  27 + actions_csv = "en,tour_plugin,.tour-button,Click"
  28 + post :index, :settings => {"actions_csv" => actions_csv}
  29 + @settings = Noosfero::Plugin::Settings.new(environment.reload, SiteTourPlugin)
  30 + assert_equal nil, @settings.settings[:actions_csv]
  31 + end
  32 +
  33 + should 'do not store group_triggers_csv' do
  34 + group_triggers_csv = "tour_plugin,.tour-button,click"
  35 + post :index, :settings => {"group_triggers_csv" => group_triggers_csv}
  36 + @settings = Noosfero::Plugin::Settings.new(environment.reload, SiteTourPlugin)
  37 + assert_equal nil, @settings.settings[:group_triggers_csv]
  38 + end
  39 +
  40 + should 'convert actions array to csv to enable user edition' do
  41 + @settings = Noosfero::Plugin::Settings.new(environment.reload, SiteTourPlugin)
  42 + @settings.actions = [{:language => 'en', :group_name => 'tour_plugin', :selector => '.tour-button', :description => 'Click'}]
  43 + @settings.save!
  44 +
  45 + get :index
  46 + assert_tag :tag => 'textarea', :attributes => {:class => 'actions-csv'}, :content => "\nen,tour_plugin,.tour-button,Click\n"
  47 + end
  48 +
  49 + should 'convert group_triggers array to csv to enable user edition' do
  50 + @settings = Noosfero::Plugin::Settings.new(environment.reload, SiteTourPlugin)
  51 + @settings.group_triggers = [{:group_name => 'tour_plugin', :selector => '.tour-button', :event => 'click'}]
  52 + @settings.save!
  53 +
  54 + get :index
  55 + assert_tag :tag => 'textarea', :attributes => {:class => 'groups-csv'}, :content => "\ntour_plugin,.tour-button,click\n"
  56 + end
  57 +
  58 +end
plugins/site_tour/test/functional/site_tour_plugin_public_controller_test.rb 0 → 100644
@@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class SiteTourPluginPublicControllerTest < ActionController::TestCase
  4 +
  5 + def setup
  6 + @person = create_user('testuser').person
  7 + end
  8 +
  9 + attr_accessor :person
  10 +
  11 + should 'not be able to mark an action if is not logged in' do
  12 + xhr :post, :mark_action, :action_name => 'test'
  13 + assert_response 401
  14 + end
  15 +
  16 + should 'be able to mark one action' do
  17 + login_as(person.identifier)
  18 + xhr :post, :mark_action, :action_name => 'test'
  19 + assert_equal({'ok' => true}, ActiveSupport::JSON.decode(response.body))
  20 + assert_equal ['test'], person.reload.site_tour_plugin_actions
  21 + end
  22 +
  23 + should 'be able to mark multiple actions' do
  24 + login_as(person.identifier)
  25 + xhr :post, :mark_action, :action_name => ['test1', 'test2']
  26 + assert_equal({'ok' => true}, ActiveSupport::JSON.decode(response.body))
  27 + assert_equal ['test1', 'test2'], person.reload.site_tour_plugin_actions
  28 + end
  29 +
  30 +end
plugins/site_tour/test/test_helper.rb 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +require File.dirname(__FILE__) + '/../../../test/test_helper'
plugins/site_tour/test/unit/site_tour_helper_test.rb 0 → 100644
@@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class SiteTourHelperTest < ActionView::TestCase
  4 +
  5 + include SiteTourPlugin::SiteTourHelper
  6 +
  7 + should 'parse tooltip description' do
  8 + assert_equal 'test', parse_tour_description("test")
  9 + end
  10 +
  11 + should 'replace profile attributes in tooltip description' do
  12 + profile = fast_create(Profile)
  13 + expects(:profile).returns(profile).at_least_once
  14 + assert_equal "name #{profile.name}, identifier #{profile.identifier}, url #{url_for profile.url}", parse_tour_description("name {profile.name}, identifier {profile.identifier}, url {profile.url}")
  15 + end
  16 +
  17 +end
plugins/site_tour/test/unit/site_tour_plugin_test.rb 0 → 100644
@@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class SiteTourPluginTest < ActionView::TestCase
  4 +
  5 + def setup
  6 + @plugin = SiteTourPlugin.new
  7 + end
  8 +
  9 + attr_accessor :plugin
  10 +
  11 + should 'include site tour plugin actions in user data for logged in users' do
  12 + expects(:logged_in?).returns(true)
  13 + person = create_user('testuser').person
  14 + person.site_tour_plugin_actions = ['login', 'navigation']
  15 + expects(:user).returns(person)
  16 +
  17 + assert_equal({:site_tour_plugin_actions => ['login', 'navigation']}, instance_eval(&plugin.user_data_extras))
  18 + end
  19 +
  20 + should 'return empty hash when user is not logged in' do
  21 + expects(:logged_in?).returns(false)
  22 + assert_equal({}, instance_eval(&plugin.user_data_extras))
  23 + end
  24 +
  25 + should 'include javascript related to tour instructions if file exists' do
  26 + file = '/plugins/site_tour/tour/pt/tour.js'
  27 + expects(:language).returns('pt')
  28 + File.expects(:exists?).with(Rails.root.join("public#{file}").to_s).returns(true)
  29 + expects(:environment).returns(Environment.default)
  30 + assert_tag_in_string instance_exec(&plugin.body_ending), :tag => 'script'
  31 + end
  32 +
  33 + should 'not include javascript file that not exists' do
  34 + file = '/plugins/site_tour/tour/pt/tour.js'
  35 + expects(:language).returns('pt')
  36 + File.expects(:exists?).with(Rails.root.join("public#{file}").to_s).returns(false)
  37 + expects(:environment).returns(Environment.default)
  38 + assert_no_tag_in_string instance_exec(&plugin.body_ending), :tag => "script"
  39 + end
  40 +
  41 + should 'render javascript tag with tooltip actions and group triggers' do
  42 + expects(:language).returns('en').at_least_once
  43 +
  44 + settings = Noosfero::Plugin::Settings.new(Environment.default, SiteTourPlugin)
  45 + settings.actions = [{:language => 'en', :group_name => 'test', :selector => 'body', :description => 'Test'}]
  46 + settings.group_triggers = [{:group_name => 'test', :selector => 'body', :event => 'click'}]
  47 + settings.save!
  48 +
  49 + expects(:environment).returns(Environment.default)
  50 + body_ending = instance_exec(&plugin.body_ending)
  51 + assert_match /siteTourPlugin\.add\('test', 'body', 'Test', 1\);/, body_ending
  52 + assert_match /siteTourPlugin\.addGroupTrigger\('test', 'body', 'click'\);/, body_ending
  53 + end
  54 +
  55 + should 'start each tooltip group with the correct step order' do
  56 + expects(:language).returns('en').at_least_once
  57 +
  58 + settings = Noosfero::Plugin::Settings.new(Environment.default, SiteTourPlugin)
  59 + settings.actions = [
  60 + {:language => 'en', :group_name => 'test_a', :selector => 'body', :description => 'Test A1'},
  61 + {:language => 'en', :group_name => 'test_a', :selector => 'body', :description => 'Test A2'},
  62 + {:language => 'en', :group_name => 'test_b', :selector => 'body', :description => 'Test B1'},
  63 + ]
  64 + settings.save!
  65 +
  66 + expects(:environment).returns(Environment.default)
  67 + body_ending = instance_exec(&plugin.body_ending)
  68 + assert_match /siteTourPlugin\.add\('test_a', 'body', 'Test A1', 1\);/, body_ending
  69 + assert_match /siteTourPlugin\.add\('test_a', 'body', 'Test A2', 2\);/, body_ending
  70 + assert_match /siteTourPlugin\.add\('test_b', 'body', 'Test B1', 3\);/, body_ending
  71 + end
  72 +
  73 +end
plugins/site_tour/test/unit/tour_block_test.rb 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class TrackListBlockTest < ActionView::TestCase
  4 +
  5 + ActionView::Base.send :include, ApplicationHelper
  6 +
  7 + def setup
  8 + @block = fast_create(SiteTourPlugin::TourBlock)
  9 + end
  10 +
  11 + attr_accessor :block
  12 +
  13 + should 'do not save empty actions' do
  14 + block.actions = [{:group_name => '', :selector => nil, :description => ' '}]
  15 + block.save!
  16 + assert_equal [], block.actions
  17 + end
  18 +
  19 + should 'render script tag in visualization mode' do
  20 + controller.expects(:boxes_editor?).returns(false)
  21 + assert_tag_in_string instance_eval(&block.content), :tag => 'script'
  22 + end
  23 +
  24 + should 'do not render script tag when editing' do
  25 + controller.expects(:boxes_editor?).returns(true)
  26 + controller.expects(:uses_design_blocks?).returns(true)
  27 + assert_no_tag_in_string instance_eval(&block.content), :tag => 'script'
  28 + end
  29 +
  30 + should 'display help button' do
  31 + controller.expects(:boxes_editor?).returns(false)
  32 + assert_tag_in_string instance_eval(&block.content), :tag => 'a', :attributes => {:class => 'button icon-help with-text tour-button'}
  33 + end
  34 +
  35 + should 'do not display help button when display_button is false' do
  36 + block.display_button = false
  37 + controller.expects(:boxes_editor?).returns(false)
  38 + assert_no_tag_in_string instance_eval(&block.content), :tag => 'a', :attributes => {:class => 'button icon-help with-text tour-button'}
  39 + end
  40 +
  41 +end
plugins/site_tour/views/blocks/tour.html.erb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +<%= block_title(block.title) %>
  2 +
  3 +<% if block.display_button %>
  4 + <%= button :help, _('Help'), '#', :class => 'tour-button', :onclick => 'siteTourPlugin.force();' %>
  5 +<% end %>
  6 +
  7 +<% edit_mode = controller.send(:boxes_editor?) && controller.send(:uses_design_blocks?) %>
  8 +
  9 +<% unless edit_mode %>
  10 + <%= render :file => 'tour_actions', :locals => {:actions => block.actions, :group_triggers => block.group_triggers} %>
  11 +<% end %>
plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block.html.erb 0 → 100644
@@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
  1 +<%= javascript_include_tag '/plugins/site_tour/edit_tour_block.js' %>
  2 +<%= stylesheet_link_tag '/plugins/site_tour/edit_tour_block.css' %>
  3 +
  4 +<%= labelled_form_field check_box(:block, :display_button) + _('Display help button'), '' %>
  5 +
  6 +<div id='edit-tour-block'>
  7 + <div id="tooltip-actions" class="list-items">
  8 + <h3><%= _('Tooltip Actions') %></h3>
  9 + <div class="special-attributes"><%= _('Special fields for description: {profile.name}, {profile.identifier}, {profile.url}.') %></div>
  10 + <ul class='list-header'>
  11 + <li class='list-name'><%= _('Group Name') %></li>
  12 + <li class='list-selector'><%= _('Selector') %></li>
  13 + <li class='list-description'><%= _('Description') %></li>
  14 + </ul>
  15 + <ul id="droppable-tour-actions" class="droppable-items">
  16 + <% for action in @block.actions do %>
  17 + <%= render :partial => 'box_organizer/site_tour_plugin/tour_block_item', :locals => {:action => action} %>
  18 + <% end %>
  19 + </ul>
  20 + <div id="new-template">
  21 + <%= render :partial => 'box_organizer/site_tour_plugin/tour_block_item', :locals => {:action => {} } %>
  22 + </div>
  23 + <%= link_to_function(_('New Tooltip'), :class => 'add-item button icon-add with-text') %>
  24 + </div>
  25 +
  26 + <div id="group-triggers" class="list-items">
  27 + <h3><%= _('Group Triggers') %></h3>
  28 + <ul class='list-header'>
  29 + <li class='list-name'><%= _('Group Name') %></li>
  30 + <li class='list-selector'><%= _('Selector') %></li>
  31 + <li class='list-event'><%= _('Event') %></li>
  32 + </ul>
  33 + <ul id="droppable-tour-group-triggers" class="droppable-items">
  34 + <% for group in @block.group_triggers do %>
  35 + <%= render :partial => 'box_organizer/site_tour_plugin/tour_block_group_item', :locals => {:group => group} %>
  36 + <% end %>
  37 + </ul>
  38 + <div id="new-template">
  39 + <%= render :partial => 'box_organizer/site_tour_plugin/tour_block_group_item', :locals => {:group => {} } %>
  40 + </div>
  41 + <%= link_to_function(_('New Group Trigger'), :class => 'add-item button icon-add with-text') %>
  42 + </div>
  43 +
  44 +</div>
plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block_group_item.html.erb 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +<li>
  2 + <ul class="item-row">
  3 + <li>
  4 + <%= text_field_tag 'block[group_triggers][][group_name]', group[:group_name], :class => 'group-name', :maxlength => 20 %>
  5 + </li>
  6 + <li>
  7 + <%= text_field_tag 'block[group_triggers][][selector]', group[:selector], :class => 'selector' %>
  8 + </li>
  9 + <li>
  10 + <%= select_tag 'block[group_triggers][][event]',
  11 + options_for_select(['mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'click', 'change', 'select', 'keydown', 'keyup', 'keypress', 'focus', 'blur', 'submit', 'drag', 'drop'], group[:event]),
  12 + :class => 'description' %>
  13 + </li>
  14 + <li>
  15 + <%= button_without_text(:delete, _('Delete'), "#" , :class=>"delete-tour-block-item") %>
  16 + </li>
  17 + </ul>
  18 +</li>
  19 +
plugins/site_tour/views/box_organizer/site_tour_plugin/_tour_block_item.html.erb 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +<li>
  2 + <ul class="item-row">
  3 + <li>
  4 + <%= text_field_tag 'block[actions][][group_name]', action[:group_name], :class => 'group-name', :maxlength => 20 %>
  5 + </li>
  6 + <li>
  7 + <%= text_field_tag 'block[actions][][selector]', action[:selector], :class => 'selector' %>
  8 + </li>
  9 + <li>
  10 + <%= text_field_tag 'block[actions][][description]', action[:description], :class => 'description' %>
  11 + </li>
  12 + <li>
  13 + <%= button_without_text(:delete, _('Delete'), "#" , :class=>"delete-tour-block-item") %>
  14 + </li>
  15 + </ul>
  16 +</li>
plugins/site_tour/views/environment_design/site_tour_plugin 0 → 120000
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +../box_organizer/site_tour_plugin
0 \ No newline at end of file 2 \ No newline at end of file
plugins/site_tour/views/profile_design/site_tour_plugin 0 → 120000
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +../box_organizer/site_tour_plugin
0 \ No newline at end of file 2 \ No newline at end of file
plugins/site_tour/views/site_tour_plugin_admin/index.html.erb 0 → 100644
@@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
  1 +<h1><%= _('Site Tour Settings')%></h1>
  2 +
  3 +<%= form_for(:settings) do |f| %>
  4 +
  5 + <%= labelled_form_field _('Tooltips (CSV format: language, group name, selector, description)'), f.text_area(:actions_csv, :style => 'width: 100%', :class => 'actions-csv') %>
  6 + <%= labelled_form_field _('Group Triggers (CSV format: group name, selector, event (e.g. mouseenter, click))'), f.text_area(:group_triggers_csv, :style => 'width: 100%', :class => 'groups-csv', :rows => 7) %>
  7 +
  8 + <% button_bar do %>
  9 + <%= submit_button(:save, _('Save'), :cancel => {:controller => 'plugins', :action => 'index'}) %>
  10 + <% end %>
  11 +
  12 +<% end %>
  13 +
plugins/site_tour/views/tour_actions.html.erb 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +<% extend SiteTourPlugin::SiteTourHelper %>
  2 +<% js_file = defined?(:js_file) ? js_file : nil %>
  3 +<%= javascript_include_tag(js_file) if js_file.present? %>
  4 +
  5 +<% if actions.present? %>
  6 +<script>
  7 + jQuery( document ).ready(function( $ ) {
  8 + <% actions.each_with_index do |action, index| %>
  9 + <%= "siteTourPlugin.add('#{j action[:group_name]}', '#{j action[:selector]}', '#{j parse_tour_description(action[:description])}', #{index + 1});" %>
  10 + <% end %>
  11 +
  12 + <% (group_triggers||[]).each do |group| %>
  13 + <%= "siteTourPlugin.addGroupTrigger('#{j group[:group_name]}', '#{j group[:selector]}', '#{j group[:event]}');" %>
  14 + <% end %>
  15 +
  16 + siteTourPlugin.setOption('nextLabel', '<%= _('Next') %>');
  17 + siteTourPlugin.setOption('prevLabel', '<%= _('Back') %>');
  18 + siteTourPlugin.setOption('skipLabel', '<%= _('Skip') %>');
  19 + siteTourPlugin.setOption('doneLabel', '<%= _('Finish') %>');
  20 + });
  21 +</script>
  22 +<% end %>
po/de/noosfero-doc.po
@@ -7,8 +7,8 @@ msgid &quot;&quot; @@ -7,8 +7,8 @@ msgid &quot;&quot;
7 msgstr "" 7 msgstr ""
8 "Project-Id-Version: PACKAGE VERSION\n" 8 "Project-Id-Version: PACKAGE VERSION\n"
9 "POT-Creation-Date: 2013-12-10 15:48-0300\n" 9 "POT-Creation-Date: 2013-12-10 15:48-0300\n"
10 -"PO-Revision-Date: 2015-02-23 11:32+0200\n"  
11 -"Last-Translator: Michal Čihař <michal@cihar.com>\n" 10 +"PO-Revision-Date: 2015-08-14 09:48+0200\n"
  11 +"Last-Translator: Phillip Rohmberger <rohmberger@hotmail.de>\n"
12 "Language-Team: German " 12 "Language-Team: German "
13 "<https://hosted.weblate.org/projects/noosfero/documentation/de/>\n" 13 "<https://hosted.weblate.org/projects/noosfero/documentation/de/>\n"
14 "Language: de\n" 14 "Language: de\n"
@@ -16,7 +16,7 @@ msgstr &quot;&quot; @@ -16,7 +16,7 @@ msgstr &quot;&quot;
16 "Content-Type: text/plain; charset=UTF-8\n" 16 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Transfer-Encoding: 8bit\n" 17 "Content-Transfer-Encoding: 8bit\n"
18 "Plural-Forms: nplurals=2; plural=n != 1;\n" 18 "Plural-Forms: nplurals=2; plural=n != 1;\n"
19 -"X-Generator: Weblate 2.3-dev\n" 19 +"X-Generator: Weblate 2.4-dev\n"
20 20
21 # type: Content of: <h1> 21 # type: Content of: <h1>
22 #. type: Content of: <h1> 22 #. type: Content of: <h1>
@@ -28,7 +28,7 @@ msgstr &quot;&quot; @@ -28,7 +28,7 @@ msgstr &quot;&quot;
28 #. type: Content of: <p> 28 #. type: Content of: <p>
29 #: doc/noosfero/plugins/send_email.en.xhtml:2 29 #: doc/noosfero/plugins/send_email.en.xhtml:2
30 msgid "Allows to send e-mails through an e-mail form." 30 msgid "Allows to send e-mails through an e-mail form."
31 -msgstr "" 31 +msgstr "Ermöglicht das Senden von E-mails über ein E-mail-Formular."
32 32
33 # type: Content of: <h2> 33 # type: Content of: <h2>
34 #. type: Content of: <h2> 34 #. type: Content of: <h2>
@@ -36,7 +36,7 @@ msgstr &quot;&quot; @@ -36,7 +36,7 @@ msgstr &quot;&quot;
36 #: doc/noosfero/plugins/google_cse.en.xhtml:3 36 #: doc/noosfero/plugins/google_cse.en.xhtml:3
37 #: doc/noosfero/plugins/google_analytics.en.xhtml:3 37 #: doc/noosfero/plugins/google_analytics.en.xhtml:3
38 msgid "Usage" 38 msgid "Usage"
39 -msgstr "" 39 +msgstr "Verwendung"
40 40
41 # type: Content of: <ul><li> 41 # type: Content of: <ul><li>
42 #. type: Content of: <ul><li> 42 #. type: Content of: <ul><li>
@@ -52,6 +52,7 @@ msgstr &quot;&quot; @@ -52,6 +52,7 @@ msgstr &quot;&quot;
52 msgid "" 52 msgid ""
53 "Add a &#8220;to&#8221; and &#8220;message&#8221; field and a submit button" 53 "Add a &#8220;to&#8221; and &#8220;message&#8221; field and a submit button"
54 msgstr "" 54 msgstr ""
  55 +"Fügt ein ``to``- und ein ``message``-Feld, mit einem ``submit`` Knopf hinzu"
55 56
56 # type: Content of: <ul><li> 57 # type: Content of: <ul><li>
57 #. type: Content of: <ul><li> 58 #. type: Content of: <ul><li>
po/pt/noosfero.po
@@ -13,7 +13,7 @@ msgid &quot;&quot; @@ -13,7 +13,7 @@ msgid &quot;&quot;
13 msgstr "" 13 msgstr ""
14 "Project-Id-Version: 1.2~rc2-15-gba5ae5b\n" 14 "Project-Id-Version: 1.2~rc2-15-gba5ae5b\n"
15 "POT-Creation-Date: 2015-08-06 17:22-0300\n" 15 "POT-Creation-Date: 2015-08-06 17:22-0300\n"
16 -"PO-Revision-Date: 2015-08-07 16:56+0200\n" 16 +"PO-Revision-Date: 2015-08-28 12:36-0300\n"
17 "Last-Translator: Antonio Terceiro <terceiro@softwarelivre.org>\n" 17 "Last-Translator: Antonio Terceiro <terceiro@softwarelivre.org>\n"
18 "Language-Team: Portuguese " 18 "Language-Team: Portuguese "
19 "<https://hosted.weblate.org/projects/noosfero/noosfero/pt/>\n" 19 "<https://hosted.weblate.org/projects/noosfero/noosfero/pt/>\n"
@@ -6176,8 +6176,8 @@ msgstr &quot;Desativar perfil&quot; @@ -6176,8 +6176,8 @@ msgstr &quot;Desativar perfil&quot;
6176 6176
6177 #: app/views/profile_editor/edit.html.erb:83 6177 #: app/views/profile_editor/edit.html.erb:83
6178 #: app/views/profile_editor/edit.html.erb:85 6178 #: app/views/profile_editor/edit.html.erb:85
6179 -msgid "Are you sure you want to deactivate this profile?"  
6180 -msgstr "Tem certeza que deseja desativar este perfil?" 6179 +msgid "Are you sure you want to activate this profile?"
  6180 +msgstr "Tem certeza que deseja ativar este perfil?"
6181 6181
6182 #: app/views/profile_editor/edit.html.erb:85 6182 #: app/views/profile_editor/edit.html.erb:85
6183 msgid "Activate profile" 6183 msgid "Activate profile"
public/designs/icons/prev_icons.yml
@@ -1,23 +0,0 @@ @@ -1,23 +0,0 @@
1 -# Define the list of icons to preview themes:  
2 -icons:  
3 - - add.png  
4 - - cancel.png  
5 - - mass_mails.png  
6 - - store.png  
7 -  
8 -# Define the main html block for each finded theme:  
9 -generate_preview_block_code: |  
10 - <div class="preview_icon_theme">  
11 - %EACH_ICON_CODE%  
12 - </div>  
13 -  
14 -# Define the html code to show each icon:  
15 -generate_each_icon_code: |  
16 - <div class="icon_num_%ICON_NUM%">  
17 - <img src="%PATH%/%ICON_NAME%" />  
18 - </div>  
19 -  
20 -# The non obvious variables are:  
21 -# %ICON_NUM% - each icon has a number equal to it's list position. It can help the layout definition.  
22 -# %PATH% - the path for the theme.  
23 -# %ICON_NAME% - the icon name without path (exatily equal the name on the icons list)  
public/designs/templates/lefttopright/javascripts/template.js
@@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
1 -$(document).ready(function() {  
2 - var box_4_height = $(".box-4").height();  
3 -  
4 - // Make box-2(the most left one) stay align with box-4  
5 - $(".box-2").css("margin-top", "-"+box_4_height+"px");  
6 -});  
public/designs/templates/lefttopright/stylesheets/style.css
1 #boxes { 1 #boxes {
2 - display: table;  
3 width: 100%; 2 width: 100%;
  3 + height: 100%;
4 } 4 }
5 5
6 -.box-1 {  
7 - width: 58%;  
8 - float: left;  
9 - margin: 1% 1% 0% 1%; 6 +.box-4 {
  7 + position: relative;
  8 + float: right;
  9 + width: 78.5%;
  10 + max-height: 400px;
  11 + overflow: hidden;
  12 + margin-left: 1%;
10 } 13 }
11 14
12 -  
13 -.box-2 { 15 +.box-3 {
  16 + width: 20.5%;
  17 + height: 100%;
  18 + min-height: 410px;
14 position: relative; 19 position: relative;
15 float: left; 20 float: left;
16 - width: 20%;  
17 } 21 }
18 22
19 -.box-3 { 23 +.box-2 {
20 position: relative; 24 position: relative;
21 float: right; 25 float: right;
22 - width: 20%; 26 + width: 20.5%;
23 margin-top: 1%; 27 margin-top: 1%;
24 } 28 }
25 29
26 -.box-4 {  
27 - float: left;  
28 - width: 79%;  
29 - margin-left: 21%;  
30 -}  
31 -  
32 -#profile-activity ul,  
33 -#profile-network ul,  
34 -#profile-wall ul {  
35 - width: 460px;  
36 -}  
37 -#profile-activity ul.comment-replies,  
38 -#profile-network ul.comment-replies,  
39 -#profile-wall ul.comment-replies {  
40 - width: auto;  
41 -}  
42 - 30 +.box-1 {
  31 + position: relative;
  32 + float: right;
  33 + width: 57%;
  34 + margin: 1% 1% 0% 1%;
  35 +}
43 \ No newline at end of file 36 \ No newline at end of file
public/designs/themes/base/style.scss
@@ -1505,7 +1505,8 @@ table#recaptcha_table tr:hover td { @@ -1505,7 +1505,8 @@ table#recaptcha_table tr:hover td {
1505 1505
1506 .event-date { 1506 .event-date {
1507 background: url('/images/calendar_date_select/calendar-icon.png') no-repeat left center; 1507 background: url('/images/calendar_date_select/calendar-icon.png') no-repeat left center;
1508 - padding: 5px; 1508 + padding: 2px;
  1509 + padding-left: 15px;
1509 } 1510 }
1510 1511
1511 .event-link { 1512 .event-link {
public/javascripts/application.js
@@ -107,11 +107,11 @@ function convToValidEmail( str ) { @@ -107,11 +107,11 @@ function convToValidEmail( str ) {
107 } 107 }
108 108
109 function updateUrlField(name_field, id) { 109 function updateUrlField(name_field, id) {
110 - url_field = $(id);  
111 - old_url_value = url_field.value; 110 + url_field = jQuery('#'+id);
  111 + old_url_value = url_field.val();
112 new_url_value = convToValidIdentifier(name_field.value, "-"); 112 new_url_value = convToValidIdentifier(name_field.value, "-");
113 113
114 - url_field.value = new_url_value; 114 + url_field.val(new_url_value);
115 115
116 if (!/^\s*$/.test(old_url_value) 116 if (!/^\s*$/.test(old_url_value)
117 && old_url_value != new_url_value 117 && old_url_value != new_url_value
test/functional/account_controller_test.rb
@@ -35,6 +35,14 @@ class AccountControllerTest &lt; ActionController::TestCase @@ -35,6 +35,14 @@ class AccountControllerTest &lt; ActionController::TestCase
35 post :login, :user => { :login => 'fake', :password => 'fake' } 35 post :login, :user => { :login => 'fake', :password => 'fake' }
36 end 36 end
37 37
  38 + should 'fail login if a user is inactive and show a warning message' do
  39 + user = User.create!(login: 'testuser', email: 'test@email.com', password:'test', password_confirmation:'test', activation_code: nil)
  40 + post :login, :user => { :login => 'testuser', :password => 'test' }
  41 +
  42 + assert_match 'not activated', session[:notice]
  43 + assert_nil session[:user]
  44 + end
  45 +
38 def test_should_fail_login_and_not_redirect 46 def test_should_fail_login_and_not_redirect
39 @request.env["HTTP_REFERER"] = 'bli' 47 @request.env["HTTP_REFERER"] = 'bli'
40 post :login, :user => {:login => 'johndoe', :password => 'bad password'} 48 post :login, :user => {:login => 'johndoe', :password => 'bad password'}
@@ -260,8 +268,9 @@ class AccountControllerTest &lt; ActionController::TestCase @@ -260,8 +268,9 @@ class AccountControllerTest &lt; ActionController::TestCase
260 assert_template 'invalid_change_password_code' 268 assert_template 'invalid_change_password_code'
261 end 269 end
262 270
263 - should 'require password confirmation correctly to enter new pasword' do 271 + should 'require password confirmation correctly to enter new password' do
264 user = create_user('testuser', :email => 'testuser@example.com', :password => 'test', :password_confirmation => 'test') 272 user = create_user('testuser', :email => 'testuser@example.com', :password => 'test', :password_confirmation => 'test')
  273 + user.activate
265 change = ChangePassword.create!(:requestor => user.person) 274 change = ChangePassword.create!(:requestor => user.person)
266 275
267 post :new_password, :code => change.code, :change_password => { :password => 'onepass', :password_confirmation => 'another_pass' } 276 post :new_password, :code => change.code, :change_password => { :password => 'onepass', :password_confirmation => 'another_pass' }
@@ -700,6 +709,8 @@ class AccountControllerTest &lt; ActionController::TestCase @@ -700,6 +709,8 @@ class AccountControllerTest &lt; ActionController::TestCase
700 get :activate 709 get :activate
701 assert_nil assigns(:message) 710 assert_nil assigns(:message)
702 post :login, :user => {:login => 'testuser', :password => 'test123'} 711 post :login, :user => {:login => 'testuser', :password => 'test123'}
  712 +
  713 + assert_match 'not activated', session[:notice]
703 assert_nil session[:user] 714 assert_nil session[:user]
704 end 715 end
705 716
@@ -709,6 +720,8 @@ class AccountControllerTest &lt; ActionController::TestCase @@ -709,6 +720,8 @@ class AccountControllerTest &lt; ActionController::TestCase
709 get :activate, :activation_code => 'wrongcode' 720 get :activate, :activation_code => 'wrongcode'
710 assert_nil assigns(:message) 721 assert_nil assigns(:message)
711 post :login, :user => {:login => 'testuser', :password => 'test123'} 722 post :login, :user => {:login => 'testuser', :password => 'test123'}
  723 +
  724 + assert_match 'not activated', session[:notice]
712 assert_nil session[:user] 725 assert_nil session[:user]
713 end 726 end
714 727
test/functional/events_controller_test.rb
@@ -8,12 +8,12 @@ class EventsControllerTest &lt; ActionController::TestCase @@ -8,12 +8,12 @@ class EventsControllerTest &lt; ActionController::TestCase
8 attr_reader :profile 8 attr_reader :profile
9 9
10 should 'list today events by default' do 10 should 'list today events by default' do
11 - profile.events << Event.new(:name => 'Joao Birthday', :start_date => Date.today)  
12 - profile.events << Event.new(:name => 'Maria Birthday', :start_date => Date.today) 11 + profile.events << Event.new(:name => 'Joao Birthday', :start_date => DateTime.now)
  12 + profile.events << Event.new(:name => 'Maria Birthday', :start_date => DateTime.now)
13 13
14 get :events, :profile => profile.identifier 14 get :events, :profile => profile.identifier
15 15
16 - today = Date.today.strftime("%B %d, %Y") 16 + today = DateTime.now.strftime("%B %d, %Y")
17 assert_tag :tag => 'div', :attributes => {:id => "agenda-items"}, 17 assert_tag :tag => 'div', :attributes => {:id => "agenda-items"},
18 :descendant => {:tag => 'h3', :content => "Events for #{today}"}, 18 :descendant => {:tag => 'h3', :content => "Events for #{today}"},
19 :descendant => {:tag => 'tr', :content => "Joao Birthday"}, 19 :descendant => {:tag => 'tr', :content => "Joao Birthday"},
@@ -23,15 +23,15 @@ class EventsControllerTest &lt; ActionController::TestCase @@ -23,15 +23,15 @@ class EventsControllerTest &lt; ActionController::TestCase
23 should 'display calendar of current month' do 23 should 'display calendar of current month' do
24 get :events, :profile => profile.identifier 24 get :events, :profile => profile.identifier
25 25
26 - month = Date.today.strftime("%B %Y") 26 + month = DateTime.now.strftime("%B %Y")
27 assert_tag :tag => 'table', :attributes => {:class => /current-month/}, :descendant => {:tag => 'caption', :content => /#{month}/} 27 assert_tag :tag => 'table', :attributes => {:class => /current-month/}, :descendant => {:tag => 'caption', :content => /#{month}/}
28 end 28 end
29 29
30 should 'display links to previous and next month' do 30 should 'display links to previous and next month' do
31 get :events, :profile => profile.identifier 31 get :events, :profile => profile.identifier
32 32
33 - prev_month = Date.today - 1.month  
34 - next_month = Date.today + 1.month 33 + prev_month = DateTime.now - 1.month
  34 + next_month = DateTime.now + 1.month
35 prev_month_name = prev_month.strftime("%B") 35 prev_month_name = prev_month.strftime("%B")
36 next_month_name = next_month.strftime("%B") 36 next_month_name = next_month.strftime("%B")
37 assert_tag :tag =>'a', :attributes => {:href => "/profile/#{profile.identifier}/events/#{prev_month.year}/#{prev_month.month}"}, :content => prev_month_name 37 assert_tag :tag =>'a', :attributes => {:href => "/profile/#{profile.identifier}/events/#{prev_month.year}/#{prev_month.month}"}, :content => prev_month_name
@@ -40,14 +40,14 @@ class EventsControllerTest &lt; ActionController::TestCase @@ -40,14 +40,14 @@ class EventsControllerTest &lt; ActionController::TestCase
40 40
41 should 'see the events paginated' do 41 should 'see the events paginated' do
42 30.times do |i| 42 30.times do |i|
43 - profile.events << Event.new(:name => "Lesson #{i}", :start_date => Date.today) 43 + profile.events << Event.new(:name => "Lesson #{i}", :start_date => DateTime.now)
44 end 44 end
45 get :events, :profile => profile.identifier 45 get :events, :profile => profile.identifier
46 assert_equal 20, assigns(:events).size 46 assert_equal 20, assigns(:events).size
47 end 47 end
48 48
49 should 'show events of specific day' do 49 should 'show events of specific day' do
50 - profile.events << Event.new(:name => 'Joao Birthday', :start_date => Date.new(2009, 10, 28)) 50 + profile.events << Event.new(:name => 'Joao Birthday', :start_date => DateTime.new(2009, 10, 28))
51 51
52 get :events_by_day, :profile => profile.identifier, :year => 2009, :month => 10, :day => 28 52 get :events_by_day, :profile => profile.identifier, :year => 2009, :month => 10, :day => 28
53 53
test/functional/search_controller_test.rb
@@ -302,7 +302,7 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -302,7 +302,7 @@ class SearchControllerTest &lt; ActionController::TestCase
302 302
303 should 'search for events' do 303 should 'search for events' do
304 person = create_user('teste').person 304 person = create_user('teste').person
305 - event = create_event(person, :name => 'an event to be found', :start_date => Date.today) 305 + event = create_event(person, :name => 'an event to be found', :start_date => DateTime.now)
306 306
307 get :events, :query => 'event to be found' 307 get :events, :query => 'event to be found'
308 308
@@ -311,10 +311,10 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -311,10 +311,10 @@ class SearchControllerTest &lt; ActionController::TestCase
311 311
312 should 'return events of the day' do 312 should 'return events of the day' do
313 person = create_user('someone').person 313 person = create_user('someone').person
314 - ten_days_ago = Date.today - 10.day 314 + ten_days_ago = DateTime.now - 10.day
315 315
316 ev1 = create_event(person, :name => 'event 1', :category_ids => [@category.id], :start_date => ten_days_ago) 316 ev1 = create_event(person, :name => 'event 1', :category_ids => [@category.id], :start_date => ten_days_ago)
317 - ev2 = create_event(person, :name => 'event 2', :category_ids => [@category.id], :start_date => Date.today - 2.month) 317 + ev2 = create_event(person, :name => 'event 2', :category_ids => [@category.id], :start_date => DateTime.now - 2.month)
318 318
319 get :events, :day => ten_days_ago.day, :month => ten_days_ago.month, :year => ten_days_ago.year 319 get :events, :day => ten_days_ago.day, :month => ten_days_ago.month, :year => ten_days_ago.year
320 assert_equal [ev1], assigns(:events) 320 assert_equal [ev1], assigns(:events)
@@ -322,9 +322,11 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -322,9 +322,11 @@ class SearchControllerTest &lt; ActionController::TestCase
322 322
323 should 'return events of the day with category' do 323 should 'return events of the day with category' do
324 person = create_user('someone').person 324 person = create_user('someone').person
325 - ten_days_ago = Date.today - 10.day 325 + ten_days_ago = DateTime.now - 10.day
326 326
327 - ev1 = create_event(person, :name => 'event 1', :category_ids => [@category.id], :start_date => ten_days_ago) 327 + ev1 = create_event(person, :name => 'event 1', :start_date => ten_days_ago)
  328 + ev1.categories = [@category]
  329 +
328 ev2 = create_event(person, :name => 'event 2', :start_date => ten_days_ago) 330 ev2 = create_event(person, :name => 'event 2', :start_date => ten_days_ago)
329 331
330 get :events, :day => ten_days_ago.day, :month => ten_days_ago.month, :year => ten_days_ago.year, :category_path => @category.path.split('/') 332 get :events, :day => ten_days_ago.day, :month => ten_days_ago.month, :year => ten_days_ago.year, :category_path => @category.path.split('/')
@@ -334,8 +336,8 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -334,8 +336,8 @@ class SearchControllerTest &lt; ActionController::TestCase
334 336
335 should 'return events of today when no date specified' do 337 should 'return events of today when no date specified' do
336 person = create_user('someone').person 338 person = create_user('someone').person
337 - ev1 = create_event(person, :name => 'event 1', :category_ids => [@category.id], :start_date => Date.today)  
338 - ev2 = create_event(person, :name => 'event 2', :category_ids => [@category.id], :start_date => Date.today - 2.month) 339 + ev1 = create_event(person, :name => 'event 1', :category_ids => [@category.id], :start_date => DateTime.now)
  340 + ev2 = create_event(person, :name => 'event 2', :category_ids => [@category.id], :start_date => DateTime.now - 2.month)
339 341
340 get :events 342 get :events
341 343
@@ -346,9 +348,9 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -346,9 +348,9 @@ class SearchControllerTest &lt; ActionController::TestCase
346 person = create_user('someone').person 348 person = create_user('someone').person
347 349
348 ev1 = create_event(person, :name => 'event 1', :category_ids => [@category.id], 350 ev1 = create_event(person, :name => 'event 1', :category_ids => [@category.id],
349 - :start_date => Date.today + 2.month) 351 + :start_date => DateTime.now + 2.month)
350 ev2 = create_event(person, :name => 'event 2', :category_ids => [@category.id], 352 ev2 = create_event(person, :name => 'event 2', :category_ids => [@category.id],
351 - :start_date => Date.today + 2.day) 353 + :start_date => DateTime.now + 2.day)
352 354
353 get :events 355 get :events
354 356
@@ -359,8 +361,8 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -359,8 +361,8 @@ class SearchControllerTest &lt; ActionController::TestCase
359 should 'list events for a given month' do 361 should 'list events for a given month' do
360 person = create_user('testuser').person 362 person = create_user('testuser').person
361 363
362 - create_event(person, :name => 'upcoming event 1', :category_ids => [@category.id], :start_date => Date.new(2008, 1, 25))  
363 - create_event(person, :name => 'upcoming event 2', :category_ids => [@category.id], :start_date => Date.new(2008, 4, 27)) 364 + create_event(person, :name => 'upcoming event 1', :category_ids => [@category.id], :start_date => DateTime.new(2008, 1, 25))
  365 + create_event(person, :name => 'upcoming event 2', :category_ids => [@category.id], :start_date => DateTime.new(2008, 4, 27))
364 366
365 get :events, :year => '2008', :month => '1' 367 get :events, :year => '2008', :month => '1'
366 368
@@ -370,7 +372,7 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -370,7 +372,7 @@ class SearchControllerTest &lt; ActionController::TestCase
370 should 'see the events paginated' do 372 should 'see the events paginated' do
371 person = create_user('testuser').person 373 person = create_user('testuser').person
372 30.times do |i| 374 30.times do |i|
373 - create_event(person, :name => "Event #{i}", :start_date => Date.today) 375 + create_event(person, :name => "Event #{i}", :start_date => DateTime.now)
374 end 376 end
375 get :events 377 get :events
376 assert_equal 20, assigns(:events).size 378 assert_equal 20, assigns(:events).size
@@ -413,7 +415,7 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -413,7 +415,7 @@ class SearchControllerTest &lt; ActionController::TestCase
413 end 415 end
414 416
415 should 'display current year/month by default as caption of current month' do 417 should 'display current year/month by default as caption of current month' do
416 - Date.expects(:today).returns(Date.new(2008, 8, 1)).at_least_once 418 + DateTime.expects(:now).returns(DateTime.new(2008, 8, 1)).at_least_once
417 419
418 get :events 420 get :events
419 assert_tag :tag => 'table', :attributes => {:class => /current-month/}, :descendant => {:tag => 'caption', :content => /August 2008/} 421 assert_tag :tag => 'table', :attributes => {:class => /current-month/}, :descendant => {:tag => 'caption', :content => /August 2008/}
@@ -472,7 +474,7 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -472,7 +474,7 @@ class SearchControllerTest &lt; ActionController::TestCase
472 474
473 should 'show events of specific day' do 475 should 'show events of specific day' do
474 person = create_user('anotheruser').person 476 person = create_user('anotheruser').person
475 - event = create_event(person, :name => 'Joao Birthday', :start_date => Date.new(2009, 10, 28)) 477 + event = create_event(person, :name => 'Joao Birthday', :start_date => DateTime.new(2009, 10, 28))
476 478
477 get :events_by_day, :year => 2009, :month => 10, :day => 28 479 get :events_by_day, :year => 2009, :month => 10, :day => 28
478 480
@@ -481,8 +483,8 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -481,8 +483,8 @@ class SearchControllerTest &lt; ActionController::TestCase
481 483
482 should 'ignore filter of events if category not exists' do 484 should 'ignore filter of events if category not exists' do
483 person = create_user('anotheruser').person 485 person = create_user('anotheruser').person
484 - create_event(person, :name => 'Joao Birthday', :start_date => Date.new(2009, 10, 28), :category_ids => [@category.id])  
485 - create_event(person, :name => 'Maria Birthday', :start_date => Date.new(2009, 10, 28)) 486 + create_event(person, :name => 'Joao Birthday', :start_date => DateTime.new(2009, 10, 28), :category_ids => [@category.id])
  487 + create_event(person, :name => 'Maria Birthday', :start_date => DateTime.new(2009, 10, 28))
486 488
487 id_of_unexistent_category = Category.last.id + 10 489 id_of_unexistent_category = Category.last.id + 10
488 490
@@ -769,7 +771,7 @@ class SearchControllerTest &lt; ActionController::TestCase @@ -769,7 +771,7 @@ class SearchControllerTest &lt; ActionController::TestCase
769 protected 771 protected
770 772
771 def create_event(profile, options) 773 def create_event(profile, options)
772 - ev = build(Event, { :name => 'some event', :start_date => Date.new(2008,1,1) }.merge(options)) 774 + ev = build(Event, { :name => 'some event', :start_date => DateTime.new(2008,1,1) }.merge(options))
773 ev.profile = profile 775 ev.profile = profile
774 ev.save! 776 ev.save!
775 ev 777 ev
test/unit/application_helper_test.rb
@@ -1022,6 +1022,27 @@ class ApplicationHelperTest &lt; ActionView::TestCase @@ -1022,6 +1022,27 @@ class ApplicationHelperTest &lt; ActionView::TestCase
1022 assert_equal "Clone Article", label_for_clone_article(TinyMceArticle.new) 1022 assert_equal "Clone Article", label_for_clone_article(TinyMceArticle.new)
1023 end 1023 end
1024 1024
  1025 + should "return top url of environment" do
  1026 + env = Environment.default
  1027 + request = mock()
  1028 + request.expects(:scheme).returns('http')
  1029 + stubs(:request).returns(request)
  1030 + stubs(:environment).returns(env)
  1031 + stubs(:profile).returns(nil)
  1032 + assert_equal env.top_url('http'), top_url
  1033 + end
  1034 +
  1035 + should "return top url considering profile" do
  1036 + env = Environment.default
  1037 + c = fast_create(Community)
  1038 + request = mock()
  1039 + request.stubs(:scheme).returns('http')
  1040 + stubs(:request).returns(request)
  1041 + stubs(:environment).returns(env)
  1042 + stubs(:profile).returns(c)
  1043 + assert_equal c.top_url, top_url
  1044 + end
  1045 +
1025 protected 1046 protected
1026 include NoosferoTestHelper 1047 include NoosferoTestHelper
1027 1048
test/unit/article_test.rb
@@ -1497,6 +1497,17 @@ class ArticleTest &lt; ActiveSupport::TestCase @@ -1497,6 +1497,17 @@ class ArticleTest &lt; ActiveSupport::TestCase
1497 assert_includes a.body_images_paths, 'http://test.com/noosfero.png' 1497 assert_includes a.body_images_paths, 'http://test.com/noosfero.png'
1498 end 1498 end
1499 1499
  1500 + should 'escape utf8 characters correctly' do
  1501 + Environment.any_instance.stubs(:default_hostname).returns('noosfero.org')
  1502 + a = build TinyMceArticle, profile: @profile
  1503 + a.body = 'Noosfero <img src="http://noosfero.com/cabeça.png" /> '
  1504 + assert_includes a.body_images_paths, 'http://noosfero.com/cabe%C3%A7a.png'
  1505 +
  1506 + # check if after save (that is, after xss_terminate run)
  1507 + a.save!
  1508 + assert_includes a.body_images_paths, 'http://noosfero.com/cabe%C3%A7a.png'
  1509 + end
  1510 +
1500 should 'get absolute images paths in article body' do 1511 should 'get absolute images paths in article body' do
1501 Environment.any_instance.stubs(:default_hostname).returns('noosfero.org') 1512 Environment.any_instance.stubs(:default_hostname).returns('noosfero.org')
1502 a = build TinyMceArticle, :profile => @profile 1513 a = build TinyMceArticle, :profile => @profile
@@ -2181,4 +2192,32 @@ class ArticleTest &lt; ActiveSupport::TestCase @@ -2181,4 +2192,32 @@ class ArticleTest &lt; ActiveSupport::TestCase
2181 article.destroy 2192 article.destroy
2182 end 2193 end
2183 2194
  2195 + should 'have can_display_media_panel with default false' do
  2196 + a = Article.new
  2197 + assert !a.can_display_media_panel?
  2198 + end
  2199 +
  2200 + should 'display media panel when allowed by the environment' do
  2201 + a = Article.new
  2202 + a.expects(:can_display_media_panel?).returns(true)
  2203 + environment = mock
  2204 + a.expects(:environment).returns(environment)
  2205 + environment.expects(:enabled?).with('media_panel').returns(true)
  2206 + assert a.display_media_panel?
  2207 + end
  2208 +
  2209 + should 'not display media panel when not allowed by the environment' do
  2210 + a = Article.new
  2211 + a.expects(:can_display_media_panel?).returns(true)
  2212 + environment = mock
  2213 + a.expects(:environment).returns(environment)
  2214 + environment.expects(:enabled?).with('media_panel').returns(false)
  2215 + assert !a.display_media_panel?
  2216 + end
  2217 +
  2218 + should 'have display_preview' do
  2219 + a = Article.new(:display_preview => false)
  2220 + assert !a.display_preview?
  2221 + end
  2222 +
2184 end 2223 end
test/unit/change_password_test.rb
@@ -29,7 +29,8 @@ class ChangePasswordTest &lt; ActiveSupport::TestCase @@ -29,7 +29,8 @@ class ChangePasswordTest &lt; ActiveSupport::TestCase
29 change.password_confirmation = 'newpass' 29 change.password_confirmation = 'newpass'
30 change.finish 30 change.finish
31 31
32 - assert User.find(person.user.id).authenticated?('newpass') 32 + person.user.activate
  33 + assert person.user.authenticated?('newpass')
33 end 34 end
34 35
35 should 'not require password and password confirmation when cancelling' do 36 should 'not require password and password confirmation when cancelling' do
test/unit/content_viewer_helper_test.rb
@@ -16,14 +16,14 @@ class ContentViewerHelperTest &lt; ActionView::TestCase @@ -16,14 +16,14 @@ class ContentViewerHelperTest &lt; ActionView::TestCase
16 blog = fast_create(Blog, :name => 'Blog test', :profile_id => profile.id) 16 blog = fast_create(Blog, :name => 'Blog test', :profile_id => profile.id)
17 post = create(TextileArticle, :name => 'post test', :profile => profile, :parent => blog) 17 post = create(TextileArticle, :name => 'post test', :profile => profile, :parent => blog)
18 result = article_title(post) 18 result = article_title(post)
19 - assert_tag_in_string result, :tag => 'span', :content => show_date(post.published_at) 19 + assert_tag_in_string result, :tag => 'span', :content => show_time(post.published_at)
20 end 20 end
21 21
22 should 'display published-at for forum posts' do 22 should 'display published-at for forum posts' do
23 forum = fast_create(Forum, :name => 'Forum test', :profile_id => profile.id) 23 forum = fast_create(Forum, :name => 'Forum test', :profile_id => profile.id)
24 post = TextileArticle.create!(:name => 'post test', :profile => profile, :parent => forum) 24 post = TextileArticle.create!(:name => 'post test', :profile => profile, :parent => forum)
25 result = article_title(post) 25 result = article_title(post)
26 - assert_tag_in_string result, :tag => 'span', :content => show_date(post.published_at) 26 + assert_tag_in_string result, :tag => 'span', :content => show_time(post.published_at)
27 end 27 end
28 28
29 should 'not display published-at for non-blog and non-forum posts' do 29 should 'not display published-at for non-blog and non-forum posts' do
test/unit/dates_helper_test.rb
@@ -21,23 +21,23 @@ class DatesHelperTest &lt; ActiveSupport::TestCase @@ -21,23 +21,23 @@ class DatesHelperTest &lt; ActiveSupport::TestCase
21 should 'generate period with two dates' do 21 should 'generate period with two dates' do
22 date1 = mock 22 date1 = mock
23 date1.stubs(:year).returns('A') 23 date1.stubs(:year).returns('A')
24 - expects(:show_date).with(date1, anything).returns('XXX') 24 + expects(:show_time).with(date1, anything).returns('XXX')
25 date2 = mock 25 date2 = mock
26 date2.stubs(:year).returns('B') 26 date2.stubs(:year).returns('B')
27 - expects(:show_date).with(date2, anything).returns('YYY') 27 + expects(:show_time).with(date2, anything).returns('YYY')
28 expects(:_).with('from %{date1} to %{date2}').returns('from %{date1} to %{date2}') 28 expects(:_).with('from %{date1} to %{date2}').returns('from %{date1} to %{date2}')
29 assert_equal 'from XXX to YYY', show_period(date1, date2) 29 assert_equal 'from XXX to YYY', show_period(date1, date2)
30 end 30 end
31 31
32 should 'generate period with in two diferent years' do 32 should 'generate period with in two diferent years' do
33 - date1 = Date.new(1920, 1, 2)  
34 - date2 = Date.new(1992, 4, 6)  
35 - assert_equal 'from January 2, 1920 to April 6, 1992', show_period(date1, date2) 33 + date1 = DateTime.new(1920, 1, 2)
  34 + date2 = DateTime.new(1992, 4, 6)
  35 + assert_equal 'from January 2, 1920 0:00 to April 6, 1992 0:00', show_period(date1, date2)
36 end 36 end
37 37
38 should 'generate period with in two diferent months of the same year' do 38 should 'generate period with in two diferent months of the same year' do
39 - date1 = Date.new(2013, 2, 1)  
40 - date2 = Date.new(2013, 3, 1) 39 + date1 = DateTime.new(2013, 2, 1)
  40 + date2 = DateTime.new(2013, 3, 1)
41 assert_equal 'from February 1 to March 1, 2013', show_period(date1, date2) 41 assert_equal 'from February 1 to March 1, 2013', show_period(date1, date2)
42 end 42 end
43 43
@@ -49,13 +49,13 @@ class DatesHelperTest &lt; ActiveSupport::TestCase @@ -49,13 +49,13 @@ class DatesHelperTest &lt; ActiveSupport::TestCase
49 49
50 should 'generate period with two equal dates' do 50 should 'generate period with two equal dates' do
51 date1 = mock 51 date1 = mock
52 - expects(:show_date).with(date1, anything).returns('XXX') 52 + expects(:show_time).with(date1, anything).returns('XXX')
53 assert_equal 'XXX', show_period(date1, date1) 53 assert_equal 'XXX', show_period(date1, date1)
54 end 54 end
55 55
56 should 'generate period with one date only' do 56 should 'generate period with one date only' do
57 date1 = mock 57 date1 = mock
58 - expects(:show_date).with(date1, anything).returns('XXX') 58 + expects(:show_time).with(date1, anything).returns('XXX')
59 assert_equal 'XXX', show_period(date1) 59 assert_equal 'XXX', show_period(date1)
60 end 60 end
61 61
@@ -84,7 +84,7 @@ class DatesHelperTest &lt; ActiveSupport::TestCase @@ -84,7 +84,7 @@ class DatesHelperTest &lt; ActiveSupport::TestCase
84 end 84 end
85 85
86 should 'fallback to current year/month in show_month' do 86 should 'fallback to current year/month in show_month' do
87 - Date.expects(:today).returns(Date.new(2008,11,1)).at_least_once 87 + DateTime.expects(:now).returns(DateTime.new(2008,11,1)).at_least_once
88 assert_equal 'November 2008', show_month(nil, nil) 88 assert_equal 'November 2008', show_month(nil, nil)
89 assert_equal 'November 2008', show_month('', '') 89 assert_equal 'November 2008', show_month('', '')
90 end 90 end
@@ -118,16 +118,16 @@ class DatesHelperTest &lt; ActiveSupport::TestCase @@ -118,16 +118,16 @@ class DatesHelperTest &lt; ActiveSupport::TestCase
118 end 118 end
119 119
120 should 'format time' do 120 should 'format time' do
121 - assert_equal '22 November 2008, 15:34', show_time(Time.mktime(2008, 11, 22, 15, 34, 0, 0)) 121 + assert_equal 'November 22, 2008 15:34', show_time(Time.mktime(2008, 11, 22, 15, 34, 0, 0))
122 end 122 end
123 123
124 should 'format time with 2 digits minutes' do 124 should 'format time with 2 digits minutes' do
125 - assert_equal '22 November 2008, 15:04', show_time(Time.mktime(2008, 11, 22, 15, 04, 0, 0)) 125 + assert_equal 'November 22, 2008 15:04', show_time(Time.mktime(2008, 11, 22, 15, 04, 0, 0))
126 end 126 end
127 127
128 should 'translate time' do 128 should 'translate time' do
129 time = Time.parse('25 May 2009, 12:47') 129 time = Time.parse('25 May 2009, 12:47')
130 - assert_equal '25 May 2009, 12:47', show_time(time) 130 + assert_equal 'May 25, 2009 12:47', show_time(time)
131 end 131 end
132 132
133 should 'handle nil time' do 133 should 'handle nil time' do
test/unit/enterprise_homepage_test.rb
@@ -26,4 +26,9 @@ class EnterpriseHomepageTest &lt; ActiveSupport::TestCase @@ -26,4 +26,9 @@ class EnterpriseHomepageTest &lt; ActiveSupport::TestCase
26 assert_equal false, a.can_display_hits? 26 assert_equal false, a.can_display_hits?
27 end 27 end
28 28
  29 + should 'have can_display_media_panel with default true' do
  30 + a = EnterpriseHomepage.new
  31 + assert a.can_display_media_panel?
  32 + end
  33 +
29 end 34 end
test/unit/event_test.rb
@@ -29,15 +29,9 @@ class EventTest &lt; ActiveSupport::TestCase @@ -29,15 +29,9 @@ class EventTest &lt; ActiveSupport::TestCase
29 assert_equal 'South Noosfero street, 88', e.address 29 assert_equal 'South Noosfero street, 88', e.address
30 end 30 end
31 31
32 - should 'have a start date' do  
33 - e = Event.new  
34 - e.start_date = Date.today  
35 - assert_kind_of Date, e.start_date  
36 - end  
37 -  
38 should 'set start date default value as today' do 32 should 'set start date default value as today' do
39 e = Event.new 33 e = Event.new
40 - assert_equal Date.today, e.start_date 34 + assert_in_delta DateTime.now.to_i, e.start_date.to_i, 1
41 end 35 end
42 36
43 should 'require start date' do 37 should 'require start date' do
@@ -45,38 +39,32 @@ class EventTest &lt; ActiveSupport::TestCase @@ -45,38 +39,32 @@ class EventTest &lt; ActiveSupport::TestCase
45 e.start_date = nil 39 e.start_date = nil
46 e.valid? 40 e.valid?
47 assert e.errors[:start_date.to_s].present? 41 assert e.errors[:start_date.to_s].present?
48 - e.start_date = Date.today 42 + e.start_date = DateTime.now
49 e.valid? 43 e.valid?
50 refute e.errors[:start_date.to_s].present? 44 refute e.errors[:start_date.to_s].present?
51 end 45 end
52 46
53 - should 'have a end date' do  
54 - e = Event.new  
55 - e.end_date = Date.today  
56 - assert_kind_of Date, e.end_date  
57 - end  
58 -  
59 should 'use its own icon' do 47 should 'use its own icon' do
60 assert_equal 'event', Event.icon_name 48 assert_equal 'event', Event.icon_name
61 end 49 end
62 50
63 should 'not allow end date before start date' do 51 should 'not allow end date before start date' do
64 - e = build(Event, :start_date => Date.new(2008, 01, 01), :end_date => Date.new(2007,01,01)) 52 + e = build(Event, :start_date => DateTime.new(2008, 01, 01), :end_date => DateTime.new(2007,01,01))
65 e.valid? 53 e.valid?
66 assert e.errors[:start_date.to_s].present? 54 assert e.errors[:start_date.to_s].present?
67 55
68 - e.end_date = Date.new(2008,01,05) 56 + e.end_date = DateTime.new(2008,01,05)
69 e.valid? 57 e.valid?
70 refute e.errors[:start_date.to_s].present? 58 refute e.errors[:start_date.to_s].present?
71 end 59 end
72 60
73 should 'find by range of dates' do 61 should 'find by range of dates' do
74 profile = create_user('testuser').person 62 profile = create_user('testuser').person
75 - e1 = create(Event, :name => 'e1', :start_date => Date.new(2008,1,1), :profile => profile)  
76 - e2 = create(Event, :name => 'e2', :start_date => Date.new(2008,2,1), :profile => profile)  
77 - e3 = create(Event, :name => 'e3', :start_date => Date.new(2008,3,1), :profile => profile) 63 + e1 = create(Event, :name => 'e1', :start_date => DateTime.new(2008,1,1), :profile => profile)
  64 + e2 = create(Event, :name => 'e2', :start_date => DateTime.new(2008,2,1), :profile => profile)
  65 + e3 = create(Event, :name => 'e3', :start_date => DateTime.new(2008,3,1), :profile => profile)
78 66
79 - found = Event.by_range(Date.new(2008, 1, 1)..Date.new(2008, 2, 28)) 67 + found = Event.by_range(DateTime.new(2008, 1, 1)..DateTime.new(2008, 2, 28))
80 assert_includes found, e1 68 assert_includes found, e1
81 assert_includes found, e2 69 assert_includes found, e2
82 assert_not_includes found, e3 70 assert_not_includes found, e3
@@ -84,32 +72,33 @@ class EventTest &lt; ActiveSupport::TestCase @@ -84,32 +72,33 @@ class EventTest &lt; ActiveSupport::TestCase
84 72
85 should 'filter events by range' do 73 should 'filter events by range' do
86 profile = create_user('testuser').person 74 profile = create_user('testuser').person
87 - e1 = create(Event, :name => 'e1', :start_date => Date.new(2008,1,15), :profile => profile)  
88 - assert_includes profile.events.by_range(Date.new(2008, 1, 10)..Date.new(2008, 1, 20)), e1 75 + e1 = create(Event, :name => 'e1', :start_date => DateTime.new(2008,1,15), :profile => profile)
  76 + assert_includes profile.events.by_range(DateTime.new(2008, 1, 10)..DateTime.new(2008, 1, 20)), e1
89 end 77 end
90 78
91 should 'provide period for searching in month' do 79 should 'provide period for searching in month' do
92 - assert_equal Date.new(2008, 1, 1)..Date.new(2008,1,31), Event.date_range(2008, 1)  
93 - assert_equal Date.new(2008, 2, 1)..Date.new(2008,2,29), Event.date_range(2008, 2)  
94 - assert_equal Date.new(2007, 2, 1)..Date.new(2007,2,28), Event.date_range(2007, 2) 80 + assert_equal DateTime.new(2008, 1, 1)..DateTime.new(2008,1,31), Event.date_range(2008, 1)
  81 + assert_equal DateTime.new(2008, 2, 1)..DateTime.new(2008,2,29), Event.date_range(2008, 2)
  82 + assert_equal DateTime.new(2007, 2, 1)..DateTime.new(2007,2,28), Event.date_range(2007, 2)
95 end 83 end
96 84
97 should 'support string arguments to Event#date_range' do 85 should 'support string arguments to Event#date_range' do
98 - assert_equal Date.new(2008,1,1)..Date.new(2008,1,31), Event.date_range('2008', '1') 86 + assert_equal DateTime.new(2008,1,1)..DateTime.new(2008,1,31), Event.date_range('2008', '1')
99 end 87 end
100 88
101 should 'provide range of dates for event with both dates filled' do 89 should 'provide range of dates for event with both dates filled' do
102 - e = build(Event, :start_date => Date.new(2008, 1, 1), :end_date => Date.new(2008, 1, 5))  
103 - assert_equal (Date.new(2008,1,1)..Date.new(2008,1,5)), e.date_range 90 + e = build(Event, :start_date => DateTime.new(2008, 1, 1), :end_date => DateTime.new(2008, 1, 5))
  91 + assert_equal (DateTime.new(2008,1,1)..DateTime.new(2008,1,5)), e.date_range
104 end 92 end
105 93
106 should 'provide range of dates for event with only start date' do 94 should 'provide range of dates for event with only start date' do
107 - e = build(Event, :start_date => Date.new(2008, 1, 1))  
108 - assert_equal (Date.new(2008,1,1)..Date.new(2008,1,1)), e.date_range 95 + e = build(Event, :start_date => DateTime.new(2008, 1, 1))
  96 + assert_equal (DateTime.new(2008,1,1)..DateTime.new(2008,1,1)), e.date_range
109 end 97 end
110 98
111 should 'provide nice display format' do 99 should 'provide nice display format' do
112 - event = build(Event, :start_date => Date.new(2008,1,1), :end_date => Date.new(2008,1,1), :link => 'http://www.myevent.org', :body => '<p>my somewhat short description</p>') 100 + date = Time.zone.local(2008, 1, 1, 0, 0, 0)
  101 + event = build(Event, :start_date => date, :end_date => date, :link => 'http://www.myevent.org', :body => '<p>my somewhat short description</p>')
113 display = instance_eval(&event.to_html) 102 display = instance_eval(&event.to_html)
114 103
115 assert_tag_in_string display, :content => Regexp.new("January 1, 2008") 104 assert_tag_in_string display, :content => Regexp.new("January 1, 2008")
@@ -148,7 +137,7 @@ class EventTest &lt; ActiveSupport::TestCase @@ -148,7 +137,7 @@ class EventTest &lt; ActiveSupport::TestCase
148 profile = create_user('testuser').person 137 profile = create_user('testuser').person
149 event = create(Event, :profile => profile, :name => 'test', 138 event = create(Event, :profile => profile, :name => 'test',
150 :body => '<p>first paragraph </p><p>second paragraph </p>', 139 :body => '<p>first paragraph </p><p>second paragraph </p>',
151 - :link => 'www.colivre.coop.br', :start_date => Date.today) 140 + :link => 'www.colivre.coop.br', :start_date => DateTime.now)
152 141
153 assert_match '<p>first paragraph </p>', event.first_paragraph 142 assert_match '<p>first paragraph </p>', event.first_paragraph
154 end 143 end
@@ -161,7 +150,7 @@ class EventTest &lt; ActiveSupport::TestCase @@ -161,7 +150,7 @@ class EventTest &lt; ActiveSupport::TestCase
161 150
162 should 'filter HTML in body' do 151 should 'filter HTML in body' do
163 profile = create_user('testuser').person 152 profile = create_user('testuser').person
164 - e = create(Event, :profile => profile, :name => 'test', :body => '<p>a paragraph (valid)</p><script type="text/javascript">/* this is invalid */</script>"', :link => 'www.colivre.coop.br', :start_date => Date.today) 153 + e = create(Event, :profile => profile, :name => 'test', :body => '<p>a paragraph (valid)</p><script type="text/javascript">/* this is invalid */</script>"', :link => 'www.colivre.coop.br', :start_date => DateTime.now)
165 154
166 assert_tag_in_string e.body, :tag => 'p', :content => 'a paragraph (valid)' 155 assert_tag_in_string e.body, :tag => 'p', :content => 'a paragraph (valid)'
167 assert_no_tag_in_string e.body, :tag => 'script' 156 assert_no_tag_in_string e.body, :tag => 'script'
@@ -169,7 +158,7 @@ class EventTest &lt; ActiveSupport::TestCase @@ -169,7 +158,7 @@ class EventTest &lt; ActiveSupport::TestCase
169 158
170 should 'filter HTML in name' do 159 should 'filter HTML in name' do
171 profile = create_user('testuser').person 160 profile = create_user('testuser').person
172 - e = create(Event, :profile => profile, :name => '<p>a paragraph (valid)</p><script type="text/javascript">/* this is invalid */</script>"', :link => 'www.colivre.coop.br', :start_date => Date.today) 161 + e = create(Event, :profile => profile, :name => '<p>a paragraph (valid)</p><script type="text/javascript">/* this is invalid */</script>"', :link => 'www.colivre.coop.br', :start_date => DateTime.now)
173 162
174 assert_tag_in_string e.name, :tag => 'p', :content => 'a paragraph (valid)' 163 assert_tag_in_string e.name, :tag => 'p', :content => 'a paragraph (valid)'
175 assert_no_tag_in_string e.name, :tag => 'script' 164 assert_no_tag_in_string e.name, :tag => 'script'
@@ -184,8 +173,8 @@ class EventTest &lt; ActiveSupport::TestCase @@ -184,8 +173,8 @@ class EventTest &lt; ActiveSupport::TestCase
184 173
185 should 'list all events' do 174 should 'list all events' do
186 profile = fast_create(Profile) 175 profile = fast_create(Profile)
187 - event1 = build(Event, :name => 'Ze Birthday', :start_date => Date.today)  
188 - event2 = build(Event, :name => 'Mane Birthday', :start_date => Date.today >> 1) 176 + event1 = build(Event, :name => 'Ze Birthday', :start_date => DateTime.now)
  177 + event2 = build(Event, :name => 'Mane Birthday', :start_date => DateTime.now >> 1)
189 profile.events << [event1, event2] 178 profile.events << [event1, event2]
190 assert_includes profile.events, event1 179 assert_includes profile.events, event1
191 assert_includes profile.events, event2 180 assert_includes profile.events, event2
@@ -194,7 +183,7 @@ class EventTest &lt; ActiveSupport::TestCase @@ -194,7 +183,7 @@ class EventTest &lt; ActiveSupport::TestCase
194 should 'list events by day' do 183 should 'list events by day' do
195 profile = fast_create(Profile) 184 profile = fast_create(Profile)
196 185
197 - today = Date.today 186 + today = DateTime.now
198 yesterday_event = build(Event, :name => 'Joao Birthday', :start_date => today - 1.day) 187 yesterday_event = build(Event, :name => 'Joao Birthday', :start_date => today - 1.day)
199 today_event = build(Event, :name => 'Ze Birthday', :start_date => today) 188 today_event = build(Event, :name => 'Ze Birthday', :start_date => today)
200 tomorrow_event = build(Event, :name => 'Mane Birthday', :start_date => today + 1.day) 189 tomorrow_event = build(Event, :name => 'Mane Birthday', :start_date => today + 1.day)
@@ -207,7 +196,7 @@ class EventTest &lt; ActiveSupport::TestCase @@ -207,7 +196,7 @@ class EventTest &lt; ActiveSupport::TestCase
207 should 'list events by month' do 196 should 'list events by month' do
208 profile = fast_create(Profile) 197 profile = fast_create(Profile)
209 198
210 - today = Date.new(2013, 10, 6) 199 + today = DateTime.new(2013, 10, 6)
211 200
212 last_month_event = Event.new(:name => 'Joao Birthday', :start_date => today - 1.month) 201 last_month_event = Event.new(:name => 'Joao Birthday', :start_date => today - 1.month)
213 202
@@ -230,7 +219,7 @@ class EventTest &lt; ActiveSupport::TestCase @@ -230,7 +219,7 @@ class EventTest &lt; ActiveSupport::TestCase
230 should 'event by month ordered by start date'do 219 should 'event by month ordered by start date'do
231 profile = fast_create(Profile) 220 profile = fast_create(Profile)
232 221
233 - today = Date.new(2013, 10, 6) 222 + today = DateTime.new(2013, 10, 6)
234 223
235 event_1 = Event.new(:name => 'Maria Birthday', :start_date => today + 1.day) 224 event_1 = Event.new(:name => 'Maria Birthday', :start_date => today + 1.day)
236 event_2 = Event.new(:name => 'Joana Birthday', :start_date => today - 1.day) 225 event_2 = Event.new(:name => 'Joana Birthday', :start_date => today - 1.day)
@@ -248,7 +237,7 @@ class EventTest &lt; ActiveSupport::TestCase @@ -248,7 +237,7 @@ class EventTest &lt; ActiveSupport::TestCase
248 should 'list events in a range' do 237 should 'list events in a range' do
249 profile = fast_create(Profile) 238 profile = fast_create(Profile)
250 239
251 - today = Date.today 240 + today = DateTime.now
252 event_in_range = build(Event, :name => 'Noosfero Conference', :start_date => today - 2.day, :end_date => today + 2.day) 241 event_in_range = build(Event, :name => 'Noosfero Conference', :start_date => today - 2.day, :end_date => today + 2.day)
253 event_in_day = build(Event, :name => 'Ze Birthday', :start_date => today) 242 event_in_day = build(Event, :name => 'Ze Birthday', :start_date => today)
254 243
@@ -262,7 +251,7 @@ class EventTest &lt; ActiveSupport::TestCase @@ -262,7 +251,7 @@ class EventTest &lt; ActiveSupport::TestCase
262 should 'not list events out of range' do 251 should 'not list events out of range' do
263 profile = fast_create(Profile) 252 profile = fast_create(Profile)
264 253
265 - today = Date.today 254 + today = DateTime.now
266 event_in_range1 = build(Event, :name => 'Foswiki Conference', :start_date => today - 2.day, :end_date => today + 2.day) 255 event_in_range1 = build(Event, :name => 'Foswiki Conference', :start_date => today - 2.day, :end_date => today + 2.day)
267 event_in_range2 = build(Event, :name => 'Debian Conference', :start_date => today - 2.day, :end_date => today + 3.day) 256 event_in_range2 = build(Event, :name => 'Debian Conference', :start_date => today - 2.day, :end_date => today + 3.day)
268 event_out_of_range = build(Event, :name => 'Ze Birthday', :start_date => today - 5.day, :end_date => today - 3.day) 257 event_out_of_range = build(Event, :name => 'Ze Birthday', :start_date => today - 5.day, :end_date => today - 3.day)
@@ -357,4 +346,9 @@ class EventTest &lt; ActiveSupport::TestCase @@ -357,4 +346,9 @@ class EventTest &lt; ActiveSupport::TestCase
357 assert event.translatable? 346 assert event.translatable?
358 end 347 end
359 348
  349 + should 'have can_display_media_panel with default true' do
  350 + a = Event.new
  351 + assert a.can_display_media_panel?
  352 + end
  353 +
360 end 354 end
test/unit/profile_test.rb
@@ -256,6 +256,20 @@ class ProfileTest &lt; ActiveSupport::TestCase @@ -256,6 +256,20 @@ class ProfileTest &lt; ActiveSupport::TestCase
256 assert_equal({:host => 'micojones.net', :profile => nil, :controller => 'content_viewer', :action => 'view_page', :page => []}, profile.url) 256 assert_equal({:host => 'micojones.net', :profile => nil, :controller => 'content_viewer', :action => 'view_page', :page => []}, profile.url)
257 end 257 end
258 258
  259 + should 'provide environment top URL when profile has not a domain' do
  260 + env = Environment.default
  261 + profile = fast_create(Profile, :environment_id => env.id)
  262 + assert_equal env.top_url, profile.top_url
  263 + end
  264 +
  265 + should 'provide top URL to profile with domain' do
  266 + env = Environment.default
  267 + profile = fast_create(Profile, :environment_id => env.id)
  268 + domain = fast_create(Domain, :name => 'example.net')
  269 + profile.domains << domain
  270 + assert_equal 'http://example.net', profile.top_url
  271 + end
  272 +
259 should 'help developers by adding a suitable port to url' do 273 should 'help developers by adding a suitable port to url' do
260 profile = build(Profile) 274 profile = build(Profile)
261 275
@@ -1566,8 +1580,8 @@ class ProfileTest &lt; ActiveSupport::TestCase @@ -1566,8 +1580,8 @@ class ProfileTest &lt; ActiveSupport::TestCase
1566 1580
1567 should 'list all events' do 1581 should 'list all events' do
1568 profile = fast_create(Profile) 1582 profile = fast_create(Profile)
1569 - event1 = Event.new(:name => 'Ze Birthday', :start_date => Date.today)  
1570 - event2 = Event.new(:name => 'Mane Birthday', :start_date => Date.today >> 1) 1583 + event1 = Event.new(:name => 'Ze Birthday', :start_date => DateTime.now)
  1584 + event2 = Event.new(:name => 'Mane Birthday', :start_date => DateTime.now >> 1)
1571 profile.events << [event1, event2] 1585 profile.events << [event1, event2]
1572 assert_includes profile.events, event1 1586 assert_includes profile.events, event1
1573 assert_includes profile.events, event2 1587 assert_includes profile.events, event2
@@ -1576,7 +1590,7 @@ class ProfileTest &lt; ActiveSupport::TestCase @@ -1576,7 +1590,7 @@ class ProfileTest &lt; ActiveSupport::TestCase
1576 should 'list events by day' do 1590 should 'list events by day' do
1577 profile = fast_create(Profile) 1591 profile = fast_create(Profile)
1578 1592
1579 - today = Date.today 1593 + today = DateTime.now
1580 yesterday_event = Event.new(:name => 'Joao Birthday', :start_date => today - 1.day) 1594 yesterday_event = Event.new(:name => 'Joao Birthday', :start_date => today - 1.day)
1581 today_event = Event.new(:name => 'Ze Birthday', :start_date => today) 1595 today_event = Event.new(:name => 'Ze Birthday', :start_date => today)
1582 tomorrow_event = Event.new(:name => 'Mane Birthday', :start_date => today + 1.day) 1596 tomorrow_event = Event.new(:name => 'Mane Birthday', :start_date => today + 1.day)
@@ -1589,7 +1603,7 @@ class ProfileTest &lt; ActiveSupport::TestCase @@ -1589,7 +1603,7 @@ class ProfileTest &lt; ActiveSupport::TestCase
1589 should 'list events by month' do 1603 should 'list events by month' do
1590 profile = fast_create(Profile) 1604 profile = fast_create(Profile)
1591 1605
1592 - today = Date.new(2014, 03, 2) 1606 + today = DateTime.new(2014, 03, 2)
1593 yesterday_event = Event.new(:name => 'Joao Birthday', :start_date => today - 1.day) 1607 yesterday_event = Event.new(:name => 'Joao Birthday', :start_date => today - 1.day)
1594 today_event = Event.new(:name => 'Ze Birthday', :start_date => today) 1608 today_event = Event.new(:name => 'Ze Birthday', :start_date => today)
1595 tomorrow_event = Event.new(:name => 'Mane Birthday', :start_date => today + 1.day) 1609 tomorrow_event = Event.new(:name => 'Mane Birthday', :start_date => today + 1.day)
@@ -1602,7 +1616,7 @@ class ProfileTest &lt; ActiveSupport::TestCase @@ -1602,7 +1616,7 @@ class ProfileTest &lt; ActiveSupport::TestCase
1602 should 'list events in a range' do 1616 should 'list events in a range' do
1603 profile = fast_create(Profile) 1617 profile = fast_create(Profile)
1604 1618
1605 - today = Date.today 1619 + today = DateTime.now
1606 event_in_range = Event.new(:name => 'Noosfero Conference', :start_date => today - 2.day, :end_date => today + 2.day) 1620 event_in_range = Event.new(:name => 'Noosfero Conference', :start_date => today - 2.day, :end_date => today + 2.day)
1607 event_in_day = Event.new(:name => 'Ze Birthday', :start_date => today) 1621 event_in_day = Event.new(:name => 'Ze Birthday', :start_date => today)
1608 1622
@@ -1616,7 +1630,7 @@ class ProfileTest &lt; ActiveSupport::TestCase @@ -1616,7 +1630,7 @@ class ProfileTest &lt; ActiveSupport::TestCase
1616 should 'not list events out of range' do 1630 should 'not list events out of range' do
1617 profile = fast_create(Profile) 1631 profile = fast_create(Profile)
1618 1632
1619 - today = Date.today 1633 + today = DateTime.now
1620 event_in_range1 = Event.new(:name => 'Foswiki Conference', :start_date => today - 2.day, :end_date => today + 2.day) 1634 event_in_range1 = Event.new(:name => 'Foswiki Conference', :start_date => today - 2.day, :end_date => today + 2.day)
1621 event_in_range2 = Event.new(:name => 'Debian Conference', :start_date => today - 2.day, :end_date => today + 3.day) 1635 event_in_range2 = Event.new(:name => 'Debian Conference', :start_date => today - 2.day, :end_date => today + 3.day)
1622 event_out_of_range = Event.new(:name => 'Ze Birthday', :start_date => today - 5.day, :end_date => today - 3.day) 1636 event_out_of_range = Event.new(:name => 'Ze Birthday', :start_date => today - 5.day, :end_date => today - 3.day)
@@ -1630,9 +1644,9 @@ class ProfileTest &lt; ActiveSupport::TestCase @@ -1630,9 +1644,9 @@ class ProfileTest &lt; ActiveSupport::TestCase
1630 1644
1631 should 'sort events by date' do 1645 should 'sort events by date' do
1632 profile = fast_create(Profile) 1646 profile = fast_create(Profile)
1633 - event1 = Event.new(:name => 'Noosfero Hackaton', :start_date => Date.today)  
1634 - event2 = Event.new(:name => 'Debian Day', :start_date => Date.today - 1)  
1635 - event3 = Event.new(:name => 'Fisl 10', :start_date => Date.today + 1) 1647 + event1 = Event.new(:name => 'Noosfero Hackaton', :start_date => DateTime.now)
  1648 + event2 = Event.new(:name => 'Debian Day', :start_date => DateTime.now - 1)
  1649 + event3 = Event.new(:name => 'Fisl 10', :start_date => DateTime.now + 1)
1636 profile.events << [event1, event2, event3] 1650 profile.events << [event1, event2, event3]
1637 assert_equal [event2, event1, event3], profile.events 1651 assert_equal [event2, event1, event3], profile.events
1638 end 1652 end
test/unit/text_article_test.rb
@@ -85,6 +85,17 @@ class TextArticleTest &lt; ActiveSupport::TestCase @@ -85,6 +85,17 @@ class TextArticleTest &lt; ActiveSupport::TestCase
85 assert_equal "<img src=\"/test.png\">", article.body 85 assert_equal "<img src=\"/test.png\">", article.body
86 end 86 end
87 87
  88 + should 'change image path to relative for profile with own domain' do
  89 + person = create_user('testuser').person
  90 + person.domains << build(Domain)
  91 +
  92 + article = TextArticle.new(:profile => person, :name => 'test')
  93 + env = Environment.default
  94 + article.body = "<img src=\"http://#{person.default_hostname}:3000/link-profile.png\">"
  95 + article.save!
  96 + assert_equal "<img src=\"/link-profile.png\">", article.body
  97 + end
  98 +
88 should 'not be translatable if there is no language available on environment' do 99 should 'not be translatable if there is no language available on environment' do
89 environment = fast_create(Environment) 100 environment = fast_create(Environment)
90 environment.languages = nil 101 environment.languages = nil
@@ -109,4 +120,12 @@ class TextArticleTest &lt; ActiveSupport::TestCase @@ -109,4 +120,12 @@ class TextArticleTest &lt; ActiveSupport::TestCase
109 assert text.translatable? 120 assert text.translatable?
110 end 121 end
111 122
  123 + should 'display preview when configured on parent that is a blog' do
  124 + person = fast_create(Person)
  125 + post = fast_create(TextArticle, :profile_id => person.id)
  126 + blog = Blog.new(:display_preview => true)
  127 + post.parent = blog
  128 + assert post.display_preview?
  129 + end
  130 +
112 end 131 end
test/unit/textile_article_test.rb
@@ -174,6 +174,11 @@ class TextileArticleTest &lt; ActiveSupport::TestCase @@ -174,6 +174,11 @@ class TextileArticleTest &lt; ActiveSupport::TestCase
174 assert_equal "<p>one\nparagraph</p>", build_article("one\nparagraph").to_html 174 assert_equal "<p>one\nparagraph</p>", build_article("one\nparagraph").to_html
175 end 175 end
176 176
  177 + should 'have can_display_media_panel with default true' do
  178 + a = TextileArticle.new
  179 + assert a.can_display_media_panel?
  180 + end
  181 +
177 protected 182 protected
178 183
179 def build_article(input = nil, options = {}) 184 def build_article(input = nil, options = {})
test/unit/tiny_mce_article_test.rb
@@ -235,4 +235,9 @@ end @@ -235,4 +235,9 @@ end
235 :attributes => { :colspan => '2', :rowspan => '3' } 235 :attributes => { :colspan => '2', :rowspan => '3' }
236 end 236 end
237 237
  238 + should 'have can_display_media_panel with default true' do
  239 + a = TinyMceArticle.new
  240 + assert a.can_display_media_panel?
  241 + end
  242 +
238 end 243 end
test/unit/user_test.rb
@@ -123,6 +123,7 @@ class UserTest &lt; ActiveSupport::TestCase @@ -123,6 +123,7 @@ class UserTest &lt; ActiveSupport::TestCase
123 123
124 def test_should_change_password 124 def test_should_change_password
125 user = create_user('changetest', :password => 'test', :password_confirmation => 'test', :email => 'changetest@example.com') 125 user = create_user('changetest', :password => 'test', :password_confirmation => 'test', :email => 'changetest@example.com')
  126 + user.activate
126 assert_nothing_raised do 127 assert_nothing_raised do
127 user.change_password!('test', 'newpass', 'newpass') 128 user.change_password!('test', 'newpass', 'newpass')
128 end 129 end
@@ -132,6 +133,7 @@ class UserTest &lt; ActiveSupport::TestCase @@ -132,6 +133,7 @@ class UserTest &lt; ActiveSupport::TestCase
132 133
133 def test_should_give_correct_current_password_for_changing_password 134 def test_should_give_correct_current_password_for_changing_password
134 user = create_user('changetest', :password => 'test', :password_confirmation => 'test', :email => 'changetest@example.com') 135 user = create_user('changetest', :password => 'test', :password_confirmation => 'test', :email => 'changetest@example.com')
  136 + user.activate
135 assert_raise User::IncorrectPassword do 137 assert_raise User::IncorrectPassword do
136 user.change_password!('wrong', 'newpass', 'newpass') 138 user.change_password!('wrong', 'newpass', 'newpass')
137 end 139 end
@@ -141,6 +143,8 @@ class UserTest &lt; ActiveSupport::TestCase @@ -141,6 +143,8 @@ class UserTest &lt; ActiveSupport::TestCase
141 143
142 should 'require matching confirmation when changing password by force' do 144 should 'require matching confirmation when changing password by force' do
143 user = create_user('changetest', :password => 'test', :password_confirmation => 'test', :email => 'changetest@example.com') 145 user = create_user('changetest', :password => 'test', :password_confirmation => 'test', :email => 'changetest@example.com')
  146 + user.activate
  147 +
144 assert_raise ActiveRecord::RecordInvalid do 148 assert_raise ActiveRecord::RecordInvalid do
145 user.force_change_password!('newpass', 'newpasswrong') 149 user.force_change_password!('newpass', 'newpasswrong')
146 end 150 end
@@ -153,6 +157,8 @@ class UserTest &lt; ActiveSupport::TestCase @@ -153,6 +157,8 @@ class UserTest &lt; ActiveSupport::TestCase
153 assert_nothing_raised do 157 assert_nothing_raised do
154 user.force_change_password!('newpass', 'newpass') 158 user.force_change_password!('newpass', 'newpass')
155 end 159 end
  160 +
  161 + user.activate
156 assert user.authenticated?('newpass') 162 assert user.authenticated?('newpass')
157 end 163 end
158 164
@@ -256,6 +262,7 @@ class UserTest &lt; ActiveSupport::TestCase @@ -256,6 +262,7 @@ class UserTest &lt; ActiveSupport::TestCase
256 262
257 # when the user logs in, her password must be reencrypted with the new 263 # when the user logs in, her password must be reencrypted with the new
258 # method 264 # method
  265 + user.activate
259 user.authenticated?('test') 266 user.authenticated?('test')
260 267
261 # and the new password must be saved back to the database 268 # and the new password must be saved back to the database
@@ -273,6 +280,7 @@ class UserTest &lt; ActiveSupport::TestCase @@ -273,6 +280,7 @@ class UserTest &lt; ActiveSupport::TestCase
273 User.expects(:system_encryption_method).returns(:md5).at_least_once 280 User.expects(:system_encryption_method).returns(:md5).at_least_once
274 281
275 # but the user provided the wrong password 282 # but the user provided the wrong password
  283 + user.activate
276 user.authenticated?('WRONG_PASSWORD') 284 user.authenticated?('WRONG_PASSWORD')
277 285
278 # and then her password is not updated 286 # and then her password is not updated
@@ -520,7 +528,9 @@ class UserTest &lt; ActiveSupport::TestCase @@ -520,7 +528,9 @@ class UserTest &lt; ActiveSupport::TestCase
520 528
521 should 'not authenticate a not activated user' do 529 should 'not authenticate a not activated user' do
522 user = new_user :login => 'testuser', :password => 'test123', :password_confirmation => 'test123' 530 user = new_user :login => 'testuser', :password => 'test123', :password_confirmation => 'test123'
523 - assert_nil User.authenticate('testuser', 'test123') 531 + assert_raises User::UserNotActivated do
  532 + User.authenticate('testuser', 'test123')
  533 + end
524 end 534 end
525 535
526 should 'have activation code but no activated at when created' do 536 should 'have activation code but no activated at when created' do