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 46  
47 47 self.current_user = plugins_alternative_authentication
48 48  
49   - self.current_user ||= User.authenticate(params[:user][:login], params[:user][:password], environment) if params[:user]
50   -
  49 + begin
  50 + self.current_user ||= User.authenticate(params[:user][:login], params[:user][:password], environment) if params[:user]
  51 + rescue User::UserNotActivated => e
  52 + session[:notice] = e.message
  53 + return
  54 + end
51 55 if logged_in?
52 56 check_join_in_community(self.current_user)
53 57  
... ...
app/controllers/public/search_controller.rb
... ... @@ -92,10 +92,10 @@ class SearchController < PublicController
92 92  
93 93 def events
94 94 if params[:year].blank? && params[:year].blank? && params[:day].blank?
95   - @date = Date.today
  95 + @date = DateTime.now
96 96 else
97   - year = (params[:year] ? params[:year].to_i : Date.today.year)
98   - month = (params[:month] ? params[:month].to_i : Date.today.month)
  97 + year = (params[:year] ? params[:year].to_i : DateTime.now.year)
  98 + month = (params[:month] ? params[:month].to_i : DateTime.now.month)
99 99 day = (params[:day] ? params[:day].to_i : 1)
100 100 @date = build_date(year, month, day)
101 101 end
... ... @@ -106,9 +106,7 @@ class SearchController < PublicController
106 106 @events = @category ?
107 107 environment.events.by_day(@date).in_category(Category.find(@category_id)).paginate(:per_page => per_page, :page => params[:page]) :
108 108 environment.events.by_day(@date).paginate(:per_page => per_page, :page => params[:page])
109   - end
110   -
111   - if params[:year] || params[:month]
  109 + elsif params[:year] || params[:month]
112 110 @events = @category ?
113 111 environment.events.by_month(@date).in_category(Category.find(@category_id)).paginate(:per_page => per_page, :page => params[:page]) :
114 112 environment.events.by_month(@date).paginate(:per_page => per_page, :page => params[:page])
... ...
app/helpers/application_helper.rb
... ... @@ -903,7 +903,7 @@ module ApplicationHelper
903 903 end
904 904  
905 905 def base_url
906   - environment.top_url(request.scheme)
  906 + profile ? profile.top_url(request.scheme) : environment.top_url(request.scheme)
907 907 end
908 908 alias :top_url :base_url
909 909  
... ...
app/helpers/blog_helper.rb
... ... @@ -6,7 +6,13 @@ module BlogHelper
6 6 @article = article
7 7 hidden_field_tag('article[published]', 1) +
8 8 hidden_field_tag('article[accept_comments]', 0) +
9   - visibility_options(article,tokenized_children)
  9 + visibility_options(article,tokenized_children) +
  10 + content_tag('h4', _('Visualization of posts')) +
  11 + content_tag(
  12 + 'div',
  13 + check_box(:article, :display_preview) +
  14 + content_tag('label', _('I want to display the preview of posts before the text'), :for => 'article_display_preview')
  15 + )
10 16 end
11 17  
12 18 def cms_label_for_new_children
... ...
app/helpers/content_viewer_helper.rb
... ... @@ -51,7 +51,7 @@ module ContentViewerHelper
51 51 elsif date_format == 'past_time'
52 52 left_time = true
53 53 end
54   - content_tag('span', show_date(article.published_at, use_numbers , year, left_time), :class => 'date')
  54 + content_tag('span', show_time(article.published_at, use_numbers , year, left_time), :class => 'date')
55 55 end
56 56  
57 57 def link_to_comments(article, args = {})
... ...
app/helpers/dates_helper.rb
... ... @@ -43,9 +43,14 @@ module DatesHelper
43 43 end
44 44  
45 45 # formats a datetime for displaying.
46   - def show_time(time)
47   - if time
48   - _('%{day} %{month} %{year}, %{hour}:%{minutes}') % { :year => time.year, :month => month_name(time.month), :day => time.day, :hour => time.hour, :minutes => time.strftime("%M") }
  46 + def show_time(time, use_numbers = false, year = true, left_time = false)
  47 + if time && use_numbers
  48 + _('%{month}/%{day}/%{year}, %{hour}:%{minutes}') % { :year => (year ? time.year : ''), :month => time.month, :day => time.day, :hour => time.hour, :minutes => time.strftime("%M") }
  49 + elsif time && left_time
  50 + date_format = time_ago_in_words(time)
  51 + elsif time
  52 + date_format = year ? _('%{month_name} %{day}, %{year} %{hour}:%{minutes}') : _('%{month_name} %{day} %{hour}:%{minutes}')
  53 + date_format % { :day => time.day, :month_name => month_name(time.month), :year => time.year, :hour => time.hour, :minutes => time.strftime("%M") }
49 54 else
50 55 ''
51 56 end
... ... @@ -53,7 +58,7 @@ module DatesHelper
53 58  
54 59 def show_period(date1, date2 = nil, use_numbers = false)
55 60 if (date1 == date2) || (date2.nil?)
56   - show_date(date1, use_numbers)
  61 + show_time(date1, use_numbers)
57 62 else
58 63 if date1.year == date2.year
59 64 if date1.month == date2.month
... ... @@ -72,8 +77,8 @@ module DatesHelper
72 77 end
73 78 else
74 79 _('from %{date1} to %{date2}') % {
75   - :date1 => show_date(date1, use_numbers),
76   - :date2 => show_date(date2, use_numbers)
  80 + :date1 => show_time(date1, use_numbers),
  81 + :date2 => show_time(date2, use_numbers)
77 82 }
78 83 end
79 84 end
... ... @@ -106,18 +111,18 @@ module DatesHelper
106 111  
107 112 def build_date(year, month, day = 1)
108 113 if year.blank? and month.blank? and day.blank?
109   - Date.today
  114 + DateTime.now
110 115 else
111 116 if year.blank?
112   - year = Date.today.year
  117 + year = DateTime.now.year
113 118 end
114 119 if month.blank?
115   - month = Date.today.month
  120 + month = DateTime.now.month
116 121 end
117 122 if day.blank?
118 123 day = 1
119 124 end
120   - Date.new(year.to_i, month.to_i, day.to_i)
  125 + DateTime.new(year.to_i, month.to_i, day.to_i)
121 126 end
122 127 end
123 128  
... ...
app/helpers/events_helper.rb
... ... @@ -16,7 +16,7 @@ module EventsHelper
16 16  
17 17 content_tag( 'tr',
18 18 content_tag('td',
19   - content_tag('div', show_date(article.start_date) + ( article.end_date.nil? ? '' : (_(" to ") + show_date(article.end_date))),:class => 'event-date' ) +
  19 + content_tag('div', show_time(article.start_date) + ( article.end_date.nil? ? '' : (_(" to ") + show_time(article.end_date))),:class => 'event-date' ) +
20 20 content_tag('div',link_to(article.name,article.url),:class => 'event-title') +
21 21 content_tag('div',(article.address.nil? or article.address == '') ? '' : (_('Place: ') + article.address),:class => 'event-place')
22 22 )
... ... @@ -30,7 +30,7 @@ module EventsHelper
30 30 # the day itself
31 31 date,
32 32 # is there any events in this date?
33   - events.any? {|event| event.date_range.include?(date)},
  33 + events.any? {|event| event.date_range.cover?(date)},
34 34 # is this date in the current month?
35 35 true
36 36 ]
... ...
app/helpers/forms_helper.rb
... ... @@ -151,7 +151,7 @@ module FormsHelper
151 151 datepicker_options[:close_text] ||= _('Done')
152 152 datepicker_options[:constrain_input] ||= true
153 153 datepicker_options[:current_text] ||= _('Today')
154   - datepicker_options[:date_format] ||= 'mm/dd/yy'
  154 + datepicker_options[:date_format] ||= 'yy/mm/dd'
155 155 datepicker_options[:day_names] ||= [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')]
156 156 datepicker_options[:day_names_min] ||= [_('Su'), _('Mo'), _('Tu'), _('We'), _('Th'), _('Fr'), _('Sa')]
157 157 datepicker_options[:day_names_short] ||= [_('Sun'), _('Mon'), _('Tue'), _('Wed'), _('Thu'), _('Fri'), _('Sat')]
... ... @@ -236,7 +236,7 @@ module FormsHelper
236 236 weekHeader: #{datepicker_options[:week_header].to_json},
237 237 yearRange: #{datepicker_options[:year_range].to_json},
238 238 yearSuffix: #{datepicker_options[:year_suffix].to_json}
239   - })
  239 + }).datepicker('setDate', new Date('#{value}'))
240 240 </script>
241 241 ".html_safe
242 242 result
... ...
app/helpers/layout_helper.rb
... ... @@ -86,7 +86,7 @@ module LayoutHelper
86 86 end
87 87  
88 88 def theme_stylesheet_path
89   - "/assets#{theme_path}/style.css"
  89 + "#{theme_path}/style.css".gsub(%r{^/}, '')
90 90 end
91 91  
92 92 def layout_template
... ...
app/models/article.rb
... ... @@ -9,7 +9,7 @@ class Article &lt; ActiveRecord::Base
9 9 :highlighted, :notify_comments, :display_hits, :slug,
10 10 :external_feed_builder, :display_versions, :external_link,
11 11 :image_builder, :show_to_followers,
12   - :author
  12 + :author, :display_preview
13 13  
14 14 acts_as_having_image
15 15  
... ... @@ -637,6 +637,20 @@ class Article &lt; ActiveRecord::Base
637 637 can_display_hits? && display_hits
638 638 end
639 639  
  640 + def display_media_panel?
  641 + can_display_media_panel? && environment.enabled?('media_panel')
  642 + end
  643 +
  644 + def can_display_media_panel?
  645 + false
  646 + end
  647 +
  648 + settings_items :display_preview, :type => :boolean, :default => false
  649 +
  650 + def display_preview?
  651 + false
  652 + end
  653 +
640 654 def image?
641 655 false
642 656 end
... ... @@ -745,9 +759,10 @@ class Article &lt; ActiveRecord::Base
745 759 end
746 760  
747 761 def body_images_paths
748   - require 'uri'
749 762 Nokogiri::HTML.fragment(self.body.to_s).css('img[src]').collect do |i|
750   - (self.profile && self.profile.environment) ? URI.join(self.profile.environment.top_url, URI.escape(i['src'])).to_s : i['src']
  763 + src = i['src']
  764 + src = URI.escape src if self.new_record? # xss_terminate runs on save
  765 + (self.profile && self.profile.environment) ? URI.join(self.profile.environment.top_url, src).to_s : src
751 766 end
752 767 end
753 768  
... ...
app/models/category.rb
... ... @@ -81,7 +81,7 @@ class Category &lt; ActiveRecord::Base
81 81 end
82 82  
83 83 def upcoming_events(limit = 10)
84   - self.events.where('start_date >= ?', Date.today).reorder('start_date').paginate(:page => 1, :per_page => limit)
  84 + self.events.where('start_date >= ?', DateTime.now.beginning_of_day).order('start_date').paginate(page: 1, per_page: limit)
85 85 end
86 86  
87 87 def display_in_menu?
... ...
app/models/enterprise_homepage.rb
... ... @@ -35,4 +35,8 @@ class EnterpriseHomepage &lt; Article
35 35 false
36 36 end
37 37  
  38 + def can_display_media_panel?
  39 + true
  40 + end
  41 +
38 42 end
... ...
app/models/environment.rb
... ... @@ -13,7 +13,7 @@ class Environment &lt; ActiveRecord::Base
13 13 :reports_lower_bound, :noreply_email,
14 14 :signup_welcome_screen_body, :members_whitelist_enabled,
15 15 :members_whitelist, :highlighted_news_amount,
16   - :portal_news_amount, :date_format
  16 + :portal_news_amount, :date_format, :signup_intro
17 17  
18 18 has_many :users
19 19  
... ...
app/models/event.rb
... ... @@ -23,7 +23,7 @@ class Event &lt; Article
23 23  
24 24 def initialize(*args)
25 25 super(*args)
26   - self.start_date ||= Date.today
  26 + self.start_date ||= DateTime.now
27 27 end
28 28  
29 29 validates_presence_of :title, :start_date
... ... @@ -35,8 +35,9 @@ class Event &lt; Article
35 35 end
36 36  
37 37 scope :by_day, -> (date) {
  38 + where('start_date >= :start_date AND start_date <= :end_date AND end_date IS NULL OR (start_date <= :end_date AND end_date >= :start_date)',
  39 + {:start_date => date.beginning_of_day, :end_date => date.end_of_day}).
38 40 order('start_date ASC')
39   - .where('start_date = :date AND end_date IS NULL OR (start_date <= :date AND end_date >= :date)', {:date => date})
40 41 }
41 42  
42 43 scope :next_events_from_month, -> (date) {
... ... @@ -75,7 +76,7 @@ class Event &lt; Article
75 76  
76 77 def self.date_range(year, month)
77 78 if year.nil? || month.nil?
78   - today = Date.today
  79 + today = DateTime.now
79 80 year = today.year
80 81 month = today.month
81 82 else
... ... @@ -83,7 +84,7 @@ class Event &lt; Article
83 84 month = month.to_i
84 85 end
85 86  
86   - first_day = Date.new(year, month, 1)
  87 + first_day = DateTime.new(year, month, 1)
87 88 last_day = first_day + 1.month - 1.day
88 89  
89 90 first_day..last_day
... ... @@ -109,7 +110,7 @@ class Event &lt; Article
109 110 end
110 111  
111 112 def duration
112   - ((self.end_date || self.start_date) - self.start_date).to_i
  113 + (((self.end_date || self.start_date) - self.start_date).to_i/60/60/24)
113 114 end
114 115  
115 116 alias_method :article_lead, :lead
... ... @@ -129,6 +130,10 @@ class Event &lt; Article
129 130 true
130 131 end
131 132  
  133 + def can_display_media_panel?
  134 + true
  135 + end
  136 +
132 137 include Noosfero::TranslatableContent
133 138 include MaybeAddHttp
134 139  
... ...
app/models/profile.rb
... ... @@ -579,6 +579,14 @@ class Profile &lt; ActiveRecord::Base
579 579 options.merge(Noosfero.url_options)
580 580 end
581 581  
  582 + def top_url(scheme = 'http')
  583 + url = scheme + '://'
  584 + url << url_options[:host]
  585 + url << ':' << url_options[:port].to_s if url_options.key?(:port)
  586 + url << Noosfero.root('')
  587 + url
  588 + end
  589 +
582 590 private :generate_url, :url_options
583 591  
584 592 def default_hostname
... ...
app/models/text_article.rb
... ... @@ -33,12 +33,16 @@ class TextArticle &lt; Article
33 33 end
34 34  
35 35 def change_element_path(el, attribute)
36   - fullpath = /(https?):\/\/(#{environment.default_hostname})(:\d+)?(\/.*)/.match(el[attribute])
  36 + fullpath = /(https?):\/\/(#{profile.default_hostname})(:\d+)?(\/.*)/.match(el[attribute])
37 37 if fullpath
38 38 domain = fullpath[2]
39 39 path = fullpath[4]
40   - el[attribute] = path if domain == environment.default_hostname
  40 + el[attribute] = path if domain == profile.default_hostname
41 41 end
42 42 end
43 43  
  44 + def display_preview?
  45 + parent && parent.kind_of?(Blog) && parent.display_preview
  46 + end
  47 +
44 48 end
... ...
app/models/textile_article.rb
... ... @@ -24,6 +24,10 @@ class TextileArticle &lt; TextArticle
24 24 true
25 25 end
26 26  
  27 + def can_display_media_panel?
  28 + true
  29 + end
  30 +
27 31 protected
28 32  
29 33 def convert_to_html(textile)
... ...
app/models/tiny_mce_article.rb
... ... @@ -28,4 +28,8 @@ class TinyMceArticle &lt; TextArticle
28 28 true
29 29 end
30 30  
  31 + def can_display_media_panel?
  32 + true
  33 + end
  34 +
31 35 end
... ...
app/models/user.rb
... ... @@ -121,11 +121,17 @@ class User &lt; ActiveRecord::Base
121 121  
122 122 validates_inclusion_of :terms_accepted, :in => [ '1' ], :if => lambda { |u| ! u.terms_of_use.blank? }, :message => N_('{fn} must be checked in order to signup.').fix_i18n
123 123  
  124 + scope :has_login?, lambda { |login,email,environment_id|
  125 + where('login = ? OR email = ?', login, email).
  126 + where(environment_id: environment_id)
  127 + }
  128 +
124 129 # Authenticates a user by their login name or email and unencrypted password. Returns the user or nil.
125 130 def self.authenticate(login, password, environment = nil)
126 131 environment ||= Environment.default
127   - u = self.where('(login = ? OR email = ?) AND environment_id = ? AND activated_at IS NOT NULL',
128   - login, login, environment.id).first # need to get the salt
  132 +
  133 + u = self.has_login?(login, login, environment.id)
  134 + u = u.first if u.is_a?(ActiveRecord::Relation)
129 135 u && u.authenticated?(password) ? u : nil
130 136 end
131 137  
... ... @@ -237,7 +243,23 @@ class User &lt; ActiveRecord::Base
237 243 password.crypt(salt)
238 244 end
239 245  
  246 + class UserNotActivated < StandardError
  247 + attr_reader :user
  248 +
  249 + def initialize(message, user = nil)
  250 + @user = user
  251 +
  252 + super(message)
  253 + end
  254 + end
  255 +
240 256 def authenticated?(password)
  257 +
  258 + unless self.activated?
  259 + message = _('The user "%{login}" is not activated! Please check your email to activate your user') % {login: self.login}
  260 + raise UserNotActivated.new(message, self)
  261 + end
  262 +
241 263 result = (crypted_password == encrypt(password))
242 264 if (encryption_method != User.system_encryption_method) && result
243 265 self.password_type = User.system_encryption_method.to_s
... ... @@ -276,9 +298,15 @@ class User &lt; ActiveRecord::Base
276 298 # current password.
277 299 # * Saves the record unless it is a new one.
278 300 def change_password!(current, new, confirmation)
279   - unless self.authenticated?(current)
280   - self.errors.add(:current_password, _('does not match.'))
281   - raise IncorrectPassword
  301 +
  302 + begin
  303 + unless self.authenticated?(current)
  304 + self.errors.add(:current_password, _('does not match.'))
  305 + raise IncorrectPassword
  306 + end
  307 + rescue UserNotActivated => e
  308 + self.errors.add(:current_password, e.message)
  309 + raise UserNotActivated
282 310 end
283 311 self.force_change_password!(new, confirmation)
284 312 end
... ...
app/views/blocks/profile_info_actions/_join_leave_community.html.erb
... ... @@ -11,7 +11,7 @@
11 11 class: 'join-community',
12 12 style: 'position: relative; display: none;' %>
13 13 <% else %>
14   - <%= button :add, _('Join this community'), profile.join_url %>
  14 + <%= button :add, _('Join this community'), profile.join_url, class: 'join-community' %>
15 15 <% end %>
16 16 <% end %>
17 17 <% else %>
... ...
app/views/cms/_event.html.erb
... ... @@ -8,9 +8,8 @@
8 8 <%= render :partial => 'general_fields' %>
9 9 <%= render :partial => 'translatable' %>
10 10  
11   -<%= labelled_form_field(_('Start date'), pick_date(:article, :start_date)) %>
  11 +<%= date_range_field('article[start_date]', 'article[end_date]', @article.start_date, @article.end_date, _('%Y-%m-%d %H:%M'), {:time => true}, {:id => 'article_start_date'} ) %>
12 12  
13   -<%= labelled_form_field(_('End date'), pick_date(:article, :end_date)) %>
14 13  
15 14 <%= labelled_form_field(_('Event website:'), text_field(:article, :link)) %>
16 15  
... ...
app/views/cms/edit.html.erb
1 1 <%= error_messages_for 'article' %>
2 2  
3   -<% show_media_panel = environment.enabled?('media_panel') && [TinyMceArticle, TextileArticle, Event, EnterpriseHomepage].any?{|klass| @article.kind_of?(klass)} %>
4   -
5   -<div class='<%= (show_media_panel ? 'with_media_panel' : 'no_media_panel') %>'>
  3 +<div class='<%= (@article.display_media_panel? ? 'with_media_panel' : 'no_media_panel') %>'>
6 4 <%= labelled_form_for 'article', :html => { :multipart => true, :class => @type } do |f| %>
7 5  
8 6 <%= hidden_field_tag("type", @type) if @type %>
... ... @@ -68,7 +66,7 @@
68 66 <% end %>
69 67 </div>
70 68  
71   -<% if show_media_panel %>
  69 +<% if @article.display_media_panel? %>
72 70 <%= render :partial => 'text_editor_sidebar' %>
73 71 <% end %>
74 72  
... ...
app/views/content_viewer/_article_title.html.erb
... ... @@ -7,7 +7,7 @@
7 7 <% end %>
8 8 </h1>
9 9 <%= render :partial => "publishing_info" %>
10   - <% unless @page.abstract.blank? %>
  10 + <% if @page.display_preview? %>
11 11 <div class="preview">
12 12 <%= @page.lead %>
13 13 </div>
... ...
app/views/content_viewer/_publishing_info.html.erb
1 1 <span class="publishing-info">
2 2 <span class="date">
3   - <%= show_date(@page.published_at) %>
  3 + <%= show_time(@page.published_at) %>
4 4 </span>
5 5 <span class="author">
6 6 <%= _(", by %s") % (@page.author ? link_to(@page.author_name, @page.author_url) : @page.author_name) %>
... ...
app/views/content_viewer/_uploaded_file.html.erb
... ... @@ -2,7 +2,7 @@
2 2 <%= link_to '',
3 3 uploaded_file.view_url,
4 4 :class => 'image',
5   - :style => 'background-image: url(%s)'% uploaded_file.public_filename(:thumb)
  5 + :style => 'background-image: url(%s)'% [Noosfero.root, uploaded_file.public_filename(:thumb)].join
6 6 %>
7 7 <span><%=h uploaded_file.title %></span>
8 8 <% else %>
... ...
db/migrate/20150722042714_change_article_date_to_datetime.rb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +class ChangeArticleDateToDatetime < ActiveRecord::Migration
  2 +
  3 + def up
  4 + change_table :articles do |t|
  5 + t.change :start_date, :datetime
  6 + t.change :end_date, :datetime
  7 + end
  8 +
  9 + change_table :article_versions do |t|
  10 + t.change :start_date, :datetime
  11 + t.change :end_date, :datetime
  12 + end
  13 + end
  14 +
  15 + def down
  16 + change_table :articles do |t|
  17 + t.change :start_date, :date
  18 + t.change :end_date, :date
  19 + end
  20 +
  21 + change_table :article_versions do |t|
  22 + t.change :start_date, :date
  23 + t.change :end_date, :date
  24 + end
  25 + end
  26 +
  27 +end
... ...
db/schema.rb
... ... @@ -11,7 +11,7 @@
11 11 #
12 12 # It's strongly recommended to check this file into your version control system.
13 13  
14   -ActiveRecord::Schema.define(:version => 20150712130827) do
  14 +ActiveRecord::Schema.define(:version => 20150722042714) do
15 15  
16 16 create_table "abuse_reports", :force => true do |t|
17 17 t.integer "reporter_id"
... ... @@ -75,8 +75,8 @@ ActiveRecord::Schema.define(:version =&gt; 20150712130827) do
75 75 t.integer "comments_count"
76 76 t.boolean "advertise", :default => true
77 77 t.boolean "published", :default => true
78   - t.date "start_date"
79   - t.date "end_date"
  78 + t.datetime "start_date"
  79 + t.datetime "end_date"
80 80 t.integer "children_count", :default => 0
81 81 t.boolean "accept_comments", :default => true
82 82 t.integer "reference_article_id"
... ... @@ -127,8 +127,8 @@ ActiveRecord::Schema.define(:version =&gt; 20150712130827) do
127 127 t.integer "comments_count", :default => 0
128 128 t.boolean "advertise", :default => true
129 129 t.boolean "published", :default => true
130   - t.date "start_date"
131   - t.date "end_date"
  130 + t.datetime "start_date"
  131 + t.datetime "end_date"
132 132 t.integer "children_count", :default => 0
133 133 t.boolean "accept_comments", :default => true
134 134 t.integer "reference_article_id"
... ...
features/events.feature
... ... @@ -223,7 +223,7 @@ Feature: events
223 223 | owner | name | start_date | end_date |
224 224 | josesilva | WikiSym 2009 | 2009-10-25 | 2009-10-27 |
225 225 When I am on /profile/josesilva/events/2009/10/26
226   - Then I should see "October 25, 2009 to October 27, 2009"
  226 + Then I should see "October 25, 2009 0:00 to October 27, 2009 0:00"
227 227  
228 228 Scenario: show place of the event
229 229 Given I am on /profile/josesilva/events/2009/10
... ...
lib/tasks/release.rake
... ... @@ -239,12 +239,12 @@ EOF
239 239 end
240 240 end
241 241  
242   - Rake::Task['noosfero:upload'].invoke
243 242 if confirm('Upload the packages')
244 243 puts "==> Uploading debian packages..."
245 244 Rake::Task['noosfero:upload_packages'].invoke(target)
246 245 else
247   - puts "I: please upload the package manually!"
  246 + puts "I: please upload the package manually later by running"
  247 + puts "I: $ rake noosfero:upload_packages"
248 248 end
249 249  
250 250 rm_f "tmp/pending-release"
... ...
plugins/analytics/controllers/myprofile/analytics_plugin/stats_controller.rb 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +class AnalyticsPlugin::StatsController < MyProfileController
  2 +
  3 + no_design_blocks
  4 +
  5 + before_filter :skip_page_view
  6 +
  7 + def index
  8 + end
  9 +
  10 + protected
  11 +
  12 + def default_url_options
  13 + # avoid rails' use_relative_controller!
  14 + {use_route: '/'}
  15 + end
  16 +
  17 + def skip_page_view
  18 + @analytics_skip_page_view = true
  19 + end
  20 +
  21 +end
... ...
plugins/analytics/lib/analytics_plugin.rb
1 1 module AnalyticsPlugin
2 2  
3   - TimeOnPageUpdateInterval = 2.minutes * 1000
  3 + TimeOnPageUpdateInterval = 2.minutes
  4 + TimeOnPageUpdateIntervalMs = TimeOnPageUpdateInterval * 1000
4 5  
5 6 extend Noosfero::Plugin::ParentMethods
6 7  
... ...
plugins/analytics/lib/analytics_plugin/base.rb
... ... @@ -38,4 +38,12 @@ class AnalyticsPlugin::Base &lt; Noosfero::Plugin
38 38 }]
39 39 end
40 40  
  41 + def control_panel_buttons
  42 + {
  43 + title: I18n.t('analytics_plugin.lib.plugin.panel_button'),
  44 + icon: 'analytics-access',
  45 + url: {controller: 'analytics_plugin/stats', action: :index}
  46 + }
  47 + end
  48 +
41 49 end
... ...
plugins/analytics/lib/ext/profile.rb
... ... @@ -13,7 +13,7 @@ end
13 13 class Profile
14 14  
15 15 def analytics_settings attrs = {}
16   - @analytics_settings ||= Noosfero::Plugin::Settings.new self, AnalyticsPlugin, attrs
  16 + @analytics_settings ||= Noosfero::Plugin::Settings.new self, ::AnalyticsPlugin, attrs
17 17 attrs.each{ |a, v| @analytics_settings.send "#{a}=", v }
18 18 @analytics_settings
19 19 end
... ...
plugins/analytics/locales/en.yml
... ... @@ -5,6 +5,13 @@ en: &amp;en
5 5 plugin:
6 6 name: 'Access tracking'
7 7 description: 'Register the access of selected profiles'
  8 + panel_button: 'Access tracking'
  9 +
  10 + views:
  11 + stats:
  12 + user: 'User'
  13 + initial_time: 'Time'
  14 + pages: 'Pages'
8 15  
9 16 en-US:
10 17 <<: *en
... ...
plugins/analytics/locales/pt.yml
... ... @@ -5,6 +5,13 @@ pt: &amp;pt
5 5 plugin:
6 6 name: 'Rastreio de accesso'
7 7 description: 'Registra o acesso de perfis selecionados'
  8 + panel_button: 'Rastreio de accesso'
  9 +
  10 + views:
  11 + stats:
  12 + user: 'Usuário'
  13 + initial_time: 'Horário'
  14 + pages: 'Páginas'
8 15  
9 16 pt-BR:
10 17 <<: *pt
... ...
plugins/analytics/models/analytics_plugin/page_view.rb
... ... @@ -25,10 +25,24 @@ class AnalyticsPlugin::PageView &lt; ActiveRecord::Base
25 25 before_validation :fill_referer_page_view, on: :create
26 26 before_validation :fill_visit, on: :create
27 27  
  28 + scope :latest, -> { order 'request_started_at DESC' }
  29 +
28 30 def request_duration
29 31 self.request_finished_at - self.request_started_at
30 32 end
31 33  
  34 + def initial_time
  35 + self.page_loaded_at || self.request_finished_at
  36 + end
  37 +
  38 + def user_last_time_seen
  39 + self.initial_time + self.time_on_page
  40 + end
  41 +
  42 + def user_on_page?
  43 + Time.now < self.user_last_time_seen + AnalyticsPlugin::TimeOnPageUpdateInterval
  44 + end
  45 +
32 46 def page_load!
33 47 self.page_loaded_at = Time.now
34 48 self.update_column :page_loaded_at, self.page_loaded_at
... ... @@ -36,10 +50,9 @@ class AnalyticsPlugin::PageView &lt; ActiveRecord::Base
36 50  
37 51 def increase_time_on_page!
38 52 now = Time.now
39   - initial_time = self.page_loaded_at || self.request_finished_at
40   - return unless now > initial_time
  53 + return unless now > self.initial_time
41 54  
42   - self.time_on_page = now - initial_time
  55 + self.time_on_page = now - self.initial_time
43 56 self.update_column :time_on_page, self.time_on_page
44 57 end
45 58  
... ... @@ -59,7 +72,7 @@ class AnalyticsPlugin::PageView &lt; ActiveRecord::Base
59 72 end
60 73  
61 74 def fill_visit
62   - self.visit = self.referer_page_view.visit if self.referer_page_view
  75 + self.visit = self.referer_page_view.visit if self.referer_page_view and self.referer_page_view.user_on_page?
63 76 self.visit ||= AnalyticsPlugin::Visit.new profile: profile
64 77 end
65 78  
... ...
plugins/analytics/models/analytics_plugin/visit.rb
... ... @@ -3,9 +3,17 @@ class AnalyticsPlugin::Visit &lt; ActiveRecord::Base
3 3 attr_accessible *self.column_names
4 4 attr_accessible :profile
5 5  
6   - default_scope -> { includes :page_views }
7   -
8 6 belongs_to :profile
9 7 has_many :page_views, class_name: 'AnalyticsPlugin::PageView', dependent: :destroy
10 8  
  9 + default_scope -> { joins(:page_views).includes :page_views }
  10 +
  11 + scope :latest, -> { order 'analytics_plugin_page_views.request_started_at DESC' }
  12 +
  13 + def first_page_view
  14 + self.page_views.first
  15 + end
  16 +
  17 + delegate :user, :initial_time, to: :first_page_view
  18 +
11 19 end
... ...
plugins/analytics/test/functional/content_viewer_controller_test.rb
... ... @@ -31,6 +31,8 @@ class ContentViewerControllerTest &lt; ActionController::TestCase
31 31  
32 32 first_page_view = @community.page_views.order(:id).first
33 33 assert_equal @request.referer, first_page_view.referer_url
  34 + assert_equal @user, first_page_view.user
  35 + assert first_page_view.request_duration > 0 and first_page_view.request_duration < 1
34 36  
35 37 @request.env['HTTP_REFERER'] = first_url
36 38 get :view_page, profile: @community.identifier, page: @community.articles.last.path.split('/')
... ... @@ -40,9 +42,13 @@ class ContentViewerControllerTest &lt; ActionController::TestCase
40 42 second_page_view = @community.page_views.order(:id).last
41 43 assert_equal first_page_view, second_page_view.referer_page_view
42 44  
43   - assert_equal @user, second_page_view.user
44   -
45   - assert second_page_view.request_duration > 0 and second_page_view.request_duration < 1
  45 + # another visit, the referer is set but should be ignored because
  46 + # the user didn't report to be on the page until now
  47 + @request.env['HTTP_REFERER'] = first_url
  48 + future = Time.now + 2*AnalyticsPlugin::TimeOnPageUpdateInterval
  49 + Time.stubs(:now).returns(future)
  50 + get :view_page, profile: @community.identifier, page: @community.articles.last.path.split('/')
  51 + assert_equal 2, @community.visits.count
46 52 end
47 53  
48 54 end
... ...
plugins/analytics/views/analytics_plugin/_body_ending.html.slim
1 1 javascript:
2 2 analytics.timeOnPage.baseUrl = #{url_for(controller: 'analytics_plugin/time_on_page').to_json}
3   - analytics.timeOnPage.updateInterval = #{AnalyticsPlugin::TimeOnPageUpdateInterval.to_json}
  3 + analytics.timeOnPage.updateInterval = #{AnalyticsPlugin::TimeOnPageUpdateIntervalMs.to_json}
4 4 analytics.requestId = #{request.env['action_dispatch.request_id'].to_json}
5 5 analytics.init()
6 6  
... ...
plugins/analytics/views/analytics_plugin/stats/_table.html.slim 0 → 100644
... ... @@ -0,0 +1,38 @@
  1 +
  2 +table#analytics-stats.table data-toggle='table' data-striped='true' data-sortable='true' data-icons-prefix='fa'
  3 + thead
  4 + - unless profile.analytics_anonymous?
  5 + th= t'analytics_plugin.views.stats.user'
  6 + th= t'analytics_plugin.views.stats.initial_time'
  7 + th= t'analytics_plugin.views.stats.pages'
  8 +
  9 + tbody
  10 + - profile.visits.each do |visit|
  11 + tr
  12 + td= link_to visit.user.name, visit.user.url
  13 + td
  14 + div data-toggle="tooltip" data-title='#{l visit.initial_time}'
  15 + = time_ago_in_words(visit.initial_time)
  16 + |&nbsp
  17 + = _'ago'
  18 + td
  19 + - visit.page_views.each do |page_view|
  20 + = link_to page_view.url, page_view.url
  21 + |&nbsp;
  22 + = "(#{distance_of_time_in_words page_view.time_on_page})"
  23 + |&nbsp;->&nbsp;
  24 +
  25 +javascript:
  26 + $('#analytics-stats').bootstrapTable({
  27 + striped: true,
  28 + columns: [
  29 + {sortable: true},
  30 + {sortable: true},
  31 + {sortable: true},
  32 + ],
  33 + })
  34 +
  35 + $(document).ready(function() {
  36 + $('[data-toggle="tooltip"]').tooltip()
  37 + })
  38 +
... ...
plugins/analytics/views/analytics_plugin/stats/index.html.slim 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +- content_for :head
  2 + = javascript_include_tag 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.8.1/bootstrap-table-all.min.js'
  3 + = stylesheet_link_tag 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.8.1/bootstrap-table.css'
  4 +
  5 += render 'table'
... ...
plugins/community_track/lib/community_track_plugin/step.rb
... ... @@ -29,8 +29,8 @@ class CommunityTrackPlugin::Step &lt; Folder
29 29  
30 30 def initialize(*args)
31 31 super(*args)
32   - self.start_date ||= Date.today
33   - self.end_date ||= Date.today + 1.day
  32 + self.start_date ||= DateTime.now
  33 + self.end_date ||= DateTime.now + 1.day
34 34 end
35 35  
36 36 def set_hidden_position
... ... @@ -72,20 +72,20 @@ class CommunityTrackPlugin::Step &lt; Folder
72 72 end
73 73  
74 74 def active?
75   - (start_date..end_date).include?(Date.today)
  75 + (start_date..end_date).cover?(DateTime.now)
76 76 end
77 77  
78 78 def finished?
79   - Date.today > end_date
  79 + DateTime.now > end_date
80 80 end
81 81  
82 82 def waiting?
83   - Date.today < start_date
  83 + DateTime.now < start_date
84 84 end
85 85  
86 86 def schedule_activation
87 87 return if !changes['start_date'] && !changes['end_date']
88   - if Date.today <= end_date || accept_comments
  88 + if DateTime.now <= end_date || accept_comments
89 89 schedule_date = !accept_comments ? start_date : end_date + 1.day
90 90 CommunityTrackPlugin::ActivationJob.find(id).destroy_all
91 91 Delayed::Job.enqueue(CommunityTrackPlugin::ActivationJob.new(self.id), :run_at => schedule_date)
... ...
plugins/community_track/test/functional/community_track_plugin_content_viewer_controller_test.rb
... ... @@ -5,7 +5,7 @@ class ContentViewerControllerTest &lt; ActionController::TestCase
5 5 def setup
6 6 @profile = Community.create!(:name => 'Sample community', :identifier => 'sample-community')
7 7 @track = create_track('track', @profile)
8   - @step = CommunityTrackPlugin::Step.create!(:name => 'step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today, :tool_type => TinyMceArticle.name)
  8 + @step = CommunityTrackPlugin::Step.create!(:name => 'step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => DateTime.now.end_of_day, :start_date => DateTime.now.beginning_of_day, :tool_type => TinyMceArticle.name)
9 9  
10 10 user = create_user('testinguser')
11 11 login_as(user.login)
... ...
plugins/community_track/test/unit/community_track_plugin/step_test.rb
... ... @@ -9,7 +9,7 @@ class StepTest &lt; ActiveSupport::TestCase
9 9 @track.add_category(@category)
10 10 @track.save!
11 11  
12   - @step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today)
  12 + @step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => DateTime.now.end_of_day, :start_date => DateTime.now.beginning_of_day - 1.day)
13 13 Delayed::Job.destroy_all
14 14 end
15 15  
... ... @@ -22,39 +22,39 @@ class StepTest &lt; ActiveSupport::TestCase
22 22 end
23 23  
24 24 should 'set accept_comments to false on create' do
25   - today = Date.today
  25 + today = DateTime.now
26 26 step = CommunityTrackPlugin::Step.create(:name => 'Step', :body => 'body', :profile => @profile, :parent => @track, :start_date => today, :end_date => today, :published => true)
27 27 refute step.accept_comments
28 28 end
29 29  
30 30 should 'do not allow step creation with a parent that is not a track' do
31   - today = Date.today
  31 + today = DateTime.now
32 32 blog = fast_create(Blog)
33 33 step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => blog, :start_date => today, :end_date => today, :published => true)
34 34 refute step.save
35 35 end
36 36  
37 37 should 'do not allow step creation without a parent' do
38   - today = Date.today
  38 + today = DateTime.now
39 39 step = CommunityTrackPlugin::Step.new(:name => 'Step', :body => 'body', :profile => @profile, :parent => nil, :start_date => today, :end_date => today, :published => true)
40 40 refute step.save
41 41 end
42 42  
43 43 should 'create step if end date is equal to start date' do
44   - @step.start_date = Date.today
45   - @step.end_date = Date.today
  44 + @step.start_date = DateTime.now
  45 + @step.end_date = DateTime.now
46 46 assert @step.save
47 47 end
48 48  
49 49 should 'create step if end date is after start date' do
50   - @step.start_date = Date.today
51   - @step.end_date = Date.today + 1.day
  50 + @step.start_date = DateTime.now
  51 + @step.end_date = DateTime.now + 1.day
52 52 assert @step.save
53 53 end
54 54  
55 55 should 'do not create step if end date is before start date' do
56   - @step.start_date = Date.today
57   - @step.end_date = Date.today - 1.day
  56 + @step.start_date = DateTime.now
  57 + @step.end_date = DateTime.now - 1.day
58 58 refute @step.save
59 59 end
60 60  
... ... @@ -71,20 +71,20 @@ class StepTest &lt; ActiveSupport::TestCase
71 71 end
72 72  
73 73 should 'be active if today is between start and end dates' do
74   - @step.start_date = Date.today
75   - @step.end_date = Date.today + 1.day
  74 + @step.start_date = DateTime.now
  75 + @step.end_date = DateTime.now + 1.day
76 76 assert @step.active?
77 77 end
78 78  
79 79 should 'be finished if today is after the end date' do
80   - @step.start_date = Date.today - 2.day
81   - @step.end_date = Date.today - 1.day
  80 + @step.start_date = DateTime.now - 2.day
  81 + @step.end_date = DateTime.now - 1.day
82 82 assert @step.finished?
83 83 end
84 84  
85 85 should 'be waiting if today is before the end date' do
86   - @step.start_date = Date.today + 1.day
87   - @step.end_date = Date.today + 2.day
  86 + @step.start_date = DateTime.now + 1.day
  87 + @step.end_date = DateTime.now + 2.day
88 88 assert @step.waiting?
89 89 end
90 90  
... ... @@ -95,17 +95,17 @@ class StepTest &lt; ActiveSupport::TestCase
95 95 end
96 96  
97 97 should 'create delayed job' do
98   - @step.start_date = Date.today
99   - @step.end_date = Date.today
  98 + @step.start_date = DateTime.now.beginning_of_day
  99 + @step.end_date = DateTime.now.end_of_day
100 100 @step.accept_comments = false
101 101 @step.schedule_activation
102 102 assert_equal 1, Delayed::Job.count
103   - assert_equal @step.start_date, Delayed::Job.first.run_at.to_date
  103 + assert_equal @step.start_date, Delayed::Job.first.run_at
104 104 end
105 105  
106 106 should 'do not duplicate delayed job' do
107   - @step.start_date = Date.today
108   - @step.end_date = Date.today
  107 + @step.start_date = DateTime.now
  108 + @step.end_date = DateTime.now
109 109 @step.schedule_activation
110 110 assert_equal 1, Delayed::Job.count
111 111 @step.schedule_activation
... ... @@ -113,30 +113,30 @@ class StepTest &lt; ActiveSupport::TestCase
113 113 end
114 114  
115 115 should 'create delayed job when a step is saved' do
116   - @step.start_date = Date.today
117   - @step.end_date = Date.today
  116 + @step.start_date = DateTime.now.beginning_of_day
  117 + @step.end_date = DateTime.now.end_of_day
118 118 @step.save!
119   - assert_equal @step.start_date, Delayed::Job.first.run_at.to_date
  119 + assert_equal @step.start_date, Delayed::Job.first.run_at
120 120 end
121 121  
122 122 should 'create delayed job even if start date has passed' do
123   - @step.start_date = Date.today - 2.days
124   - @step.end_date = Date.today
  123 + @step.start_date = DateTime.now - 2.days
  124 + @step.end_date = DateTime.now.end_of_day
125 125 @step.accept_comments = false
126 126 @step.schedule_activation
127   - assert_equal @step.start_date, Delayed::Job.first.run_at.to_date
  127 + assert_equal @step.start_date, Delayed::Job.first.run_at
128 128 end
129 129  
130 130 should 'create delayed job if end date has passed' do
131   - @step.start_date = Date.today - 5.days
132   - @step.end_date = Date.today - 2.days
  131 + @step.start_date = DateTime.now - 5.days
  132 + @step.end_date = DateTime.now - 2.days
133 133 @step.schedule_activation
134   - assert_equal @step.end_date + 1.day, Delayed::Job.first.run_at.to_date
  134 + assert_equal @step.end_date + 1.day, Delayed::Job.first.run_at
135 135 end
136 136  
137 137 should 'do not schedule delayed job if save but do not modify date fields' do
138   - @step.start_date = Date.today
139   - @step.end_date = Date.today
  138 + @step.start_date = DateTime.now
  139 + @step.end_date = DateTime.now.end_of_day
140 140 @step.save!
141 141 assert_equal 1, Delayed::Job.count
142 142 Delayed::Job.destroy_all
... ... @@ -149,13 +149,13 @@ class StepTest &lt; ActiveSupport::TestCase
149 149 refute @step.position
150 150 @step.save!
151 151 assert_equal 1, @step.position
152   - step2 = CommunityTrackPlugin::Step.new(:name => 'Step2', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today)
  152 + step2 = CommunityTrackPlugin::Step.new(:name => 'Step2', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => DateTime.now.end_of_day, :start_date => DateTime.now.beginning_of_day)
153 153 step2.save!
154 154 assert_equal 2, step2.position
155 155 end
156 156  
157 157 should 'accept comments if step is active' do
158   - @step.start_date = Date.today
  158 + @step.start_date = DateTime.now
159 159 @step.save!
160 160 refute @step.accept_comments
161 161 @step.toggle_activation
... ... @@ -164,8 +164,8 @@ class StepTest &lt; ActiveSupport::TestCase
164 164 end
165 165  
166 166 should 'do not accept comments if step is not active' do
167   - @step.start_date = Date.today + 2.days
168   - @step.end_date = Date.today + 3.days
  167 + @step.start_date = DateTime.now + 2.days
  168 + @step.end_date = DateTime.now + 3.days
169 169 @step.save!
170 170 refute @step.published
171 171 @step.toggle_activation
... ... @@ -174,14 +174,14 @@ class StepTest &lt; ActiveSupport::TestCase
174 174 end
175 175  
176 176 should 'do not accept comments if step is not active anymore' do
177   - @step.start_date = Date.today
  177 + @step.end_date = DateTime.now.end_of_day
178 178 @step.save!
179 179 @step.toggle_activation
180 180 @step.reload
181 181 assert @step.accept_comments
182 182  
183   - @step.start_date = Date.today - 2.days
184   - @step.end_date = Date.today - 1.day
  183 + @step.start_date = DateTime.now - 2.days
  184 + @step.end_date = DateTime.now - 1.day
185 185 @step.save!
186 186 @step.toggle_activation
187 187 @step.reload
... ... @@ -203,7 +203,7 @@ class StepTest &lt; ActiveSupport::TestCase
203 203 end
204 204  
205 205 should 'change position to botton if a hidden step becomes visible' do
206   - step1 = CommunityTrackPlugin::Step.new(:name => 'Step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today)
  206 + step1 = CommunityTrackPlugin::Step.new(:name => 'Step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => DateTime.now.end_of_day, :start_date => DateTime.now.beginning_of_day)
207 207 step1.save!
208 208 @step.hidden = true
209 209 @step.save!
... ... @@ -215,7 +215,7 @@ class StepTest &lt; ActiveSupport::TestCase
215 215  
216 216 should 'decrement lower items positions if a step becomes hidden' do
217 217 @step.save!
218   - step1 = CommunityTrackPlugin::Step.new(:name => 'Step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => Date.today, :start_date => Date.today)
  218 + step1 = CommunityTrackPlugin::Step.new(:name => 'Step1', :body => 'body', :profile => @profile, :parent => @track, :published => false, :end_date => DateTime.now.end_of_day, :start_date => DateTime.now.beginning_of_day)
219 219 step1.save!
220 220 assert_equal 2, step1.position
221 221 @step.hidden = true
... ... @@ -225,7 +225,7 @@ class StepTest &lt; ActiveSupport::TestCase
225 225 end
226 226  
227 227 should 'do not publish a hidden step' do
228   - @step.start_date = Date.today
  228 + @step.start_date = DateTime.now
229 229 @step.hidden = true
230 230 @step.save!
231 231 refute @step.published
... ... @@ -266,7 +266,7 @@ class StepTest &lt; ActiveSupport::TestCase
266 266 end
267 267  
268 268 should 'enable comments on children when step is activated' do
269   - @step.start_date = Date.today
  269 + @step.start_date = DateTime.now
270 270 @step.save!
271 271 refute @step.accept_comments
272 272 article = fast_create(Article, :parent_id => @step.id, :profile_id => @step.profile.id, :accept_comments => false)
... ... @@ -276,8 +276,7 @@ class StepTest &lt; ActiveSupport::TestCase
276 276 end
277 277  
278 278 should 'enable comments on children when step is active' do
279   - @step.start_date = Date.today
280   - @step.start_date = Date.today
  279 + @step.start_date = DateTime.now
281 280 @step.save!
282 281 refute @step.accept_comments
283 282 @step.toggle_activation
... ...
plugins/event/lib/event_plugin/event_block.rb
... ... @@ -30,13 +30,13 @@ class EventPlugin::EventBlock &lt; Block
30 30 events = user.nil? ? events.is_public : events.display_filter(user,nil)
31 31  
32 32 if future_only
33   - events = events.where('start_date >= ?', Date.today)
  33 + events = events.where('start_date >= ?', DateTime.now.beginning_of_day)
34 34 end
35 35  
36 36 if date_distance_limit > 0
37 37 events = events.by_range([
38   - Date.today - date_distance_limit,
39   - Date.today + date_distance_limit
  38 + DateTime.now.beginning_of_day - date_distance_limit,
  39 + DateTime.now.beginning_of_day + date_distance_limit
40 40 ])
41 41 end
42 42  
... ...
plugins/event/test/functional/event_block_test.rb
... ... @@ -7,7 +7,7 @@ class HomeControllerTest &lt; ActionController::TestCase
7 7 @env.enable_plugin('EventPlugin')
8 8  
9 9 @p1 = fast_create(Person, :environment_id => @env.id)
10   - @e1a = fast_create(Event, :name=>'Event p1 A', :profile_id=>@p1.id)
  10 + @e1a = Event.create!(:name=>'Event p1 A', :profile =>@p1)
11 11  
12 12 box = Box.create!(:owner => @env)
13 13 @block = EventPlugin::EventBlock.create!(:box => box)
... ... @@ -19,6 +19,7 @@ class HomeControllerTest &lt; ActionController::TestCase
19 19  
20 20 should 'see events microdata sturcture' do
21 21 get :index
  22 +#raise response.body.inspect
22 23 assert_select '.event-plugin_event-block ul.events'
23 24 assert_select ev
24 25 assert_select ev + 'a[itemprop="url"]'
... ... @@ -33,15 +34,15 @@ class HomeControllerTest &lt; ActionController::TestCase
33 34  
34 35 should 'see event duration' do
35 36 @e1a.slug = 'event1a'
36   - @e1a.start_date = Date.today
37   - @e1a.end_date = Date.today + 1.day
  37 + @e1a.start_date = DateTime.now
  38 + @e1a.end_date = DateTime.now + 1.day
38 39 @e1a.save!
39 40 get :index
40 41 assert_select ev + 'time.duration[itemprop="endDate"]', /1 day/
41 42  
42 43 @e1a.slug = 'event1a'
43   - @e1a.start_date = Date.today
44   - @e1a.end_date = Date.today + 2.day
  44 + @e1a.start_date = DateTime.now
  45 + @e1a.end_date = DateTime.now + 2.day
45 46 @e1a.save!
46 47 get :index
47 48 assert_select ev + 'time.duration[itemprop="endDate"]', /2 days/
... ... @@ -52,8 +53,8 @@ class HomeControllerTest &lt; ActionController::TestCase
52 53 assert_select ev + 'time.duration[itemprop="endDate"]', false
53 54  
54 55 @e1a.slug = 'event1a'
55   - @e1a.start_date = Date.today
56   - @e1a.end_date = Date.today
  56 + @e1a.start_date = DateTime.now
  57 + @e1a.end_date = DateTime.now
57 58 @e1a.save!
58 59 get :index
59 60 assert_select ev + 'time.duration[itemprop="endDate"]', false
... ...
plugins/event/views/blocks/event.html.erb
... ... @@ -2,7 +2,7 @@
2 2  
3 3 <ul class="events">
4 4 <% block.events(user).map do |event| %>
5   - <% days_left = ( event.start_date - Date.today ).round %>
  5 + <% days_left = ( (event.start_date - DateTime.now)/60/60/24 ).round %>
6 6 <li itemscope="itemscope" itemtype="http://data-vocabulary.org/Event" class="event">
7 7 <%= render(
8 8 :file => 'event_plugin/event_block_item',
... ...
plugins/fb_app/Gemfile 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +gem 'slim'
  2 +
  3 +# for backwards compatibility of serialized objects
  4 +gem 'fb_graph'
  5 +
  6 +gem 'fb_graph2'
  7 +
  8 +gem 'facebook-signed-request'
  9 +
  10 +
... ...
plugins/fb_app/config.yml.dist 0 → 100644
... ... @@ -0,0 +1,47 @@
  1 +page_tab:
  2 + use_test_app: false
  3 +timeline:
  4 + use_test_app: true
  5 +
  6 +test_users:
  7 + - identifier1
  8 + - identifier1
  9 +
  10 +app:
  11 + id: xxx
  12 + secret: xxx
  13 + domain: domainconfigured.net
  14 +
  15 + open_graph:
  16 + namespace: app_name
  17 + objects:
  18 + blog_post: article
  19 + community: community
  20 + enterprise: sse_initiative
  21 + favorite_enterprise: sse_initiative
  22 + forum: discussion
  23 + event: event
  24 + friend: friend
  25 + gallery_image: picture
  26 + person: user
  27 + product: sse_product
  28 + uploaded_file: document
  29 + actions:
  30 + add: add
  31 + comment: comment
  32 + create: create
  33 + favorite: favorite
  34 + like: like
  35 + make_friendship: make_friendship
  36 + upload: upload
  37 + update: update
  38 + start: start
  39 + announce_creation: announce_creation
  40 + announce_new: announce_new
  41 + announce_update: announce_update
  42 + announce_news: announce_news
  43 +
  44 +test_app:
  45 + test_id: xxx
  46 + test_secret: xxx
  47 +
... ...
plugins/fb_app/controllers/myprofile/fb_app_plugin_myprofile_controller.rb 0 → 100644
... ... @@ -0,0 +1,59 @@
  1 +class FbAppPluginMyprofileController < OpenGraphPlugin::MyprofileController
  2 +
  3 + no_design_blocks
  4 +
  5 + before_filter :load_provider
  6 + before_filter :load_auth
  7 +
  8 + def index
  9 + if params[:tabs_added]
  10 + @page_tabs = FbAppPlugin::PageTab.create_from_tabs_added params[:tabs_added], params[:page_tab]
  11 + @page_tab = @page_tabs.first
  12 + redirect_to @page_tab.facebook_url
  13 + end
  14 + end
  15 +
  16 + def show_login
  17 + @status = params[:auth].delete :status
  18 + @logged_auth = FbAppPlugin::Auth.new params[:auth]
  19 + @logged_auth.fetch_user
  20 + if @auth.connected?
  21 + render partial: 'identity', locals: {auth: @logged_auth}
  22 + else
  23 + render nothing: true
  24 + end
  25 + end
  26 +
  27 + def save_auth
  28 + @status = params[:auth].delete :status rescue FbAppPlugin::Auth::Status::Unknown
  29 + if @status == FbAppPlugin::Auth::Status::Connected
  30 + @auth.attributes = params[:auth]
  31 + @auth.save! if @auth.changed?
  32 + else
  33 + @auth.destroy if @auth and @auth.persisted?
  34 + @auth = new_auth
  35 + end
  36 +
  37 + render partial: 'settings'
  38 + end
  39 +
  40 + protected
  41 +
  42 + def load_provider
  43 + @provider = FbAppPlugin.oauth_provider_for environment
  44 + end
  45 +
  46 + def load_auth
  47 + @auth = FbAppPlugin::Auth.where(profile_id: profile.id, provider_id: @provider.id).first
  48 + @auth ||= new_auth
  49 + end
  50 +
  51 + def new_auth
  52 + FbAppPlugin::Auth.new profile: profile, provider: @provider
  53 + end
  54 +
  55 + def context
  56 + :fb_app
  57 + end
  58 +
  59 +end
... ...
plugins/fb_app/controllers/public/fb_app_plugin_controller.rb 0 → 100644
... ... @@ -0,0 +1,24 @@
  1 +class FbAppPluginController < PublicController
  2 +
  3 + no_design_blocks
  4 +
  5 + def index
  6 + end
  7 +
  8 + def myprofile_config
  9 + if logged_in?
  10 + redirect_to controller: :fb_app_plugin_myprofile, profile: user.identifier
  11 + else
  12 + redirect_to controller: :account, action: :login, return_to: url_for(controller: :fb_app_plugin, action: :myprofile_config)
  13 + end
  14 + end
  15 +
  16 + protected
  17 +
  18 + # prevent session reset because X-CSRF not being passed by FB
  19 + # see also https://gist.github.com/toretore/911886
  20 + def handle_unverified_request
  21 + end
  22 +
  23 +end
  24 +
... ...
plugins/fb_app/controllers/public/fb_app_plugin_page_tab_controller.rb 0 → 100644
... ... @@ -0,0 +1,173 @@
  1 +class FbAppPluginPageTabController < FbAppPluginController
  2 +
  3 + no_design_blocks
  4 +
  5 + before_filter :change_theme
  6 + before_filter :disable_cache
  7 +
  8 + include CatalogHelper
  9 +
  10 + helper ManageProductsHelper
  11 + helper FbAppPlugin::DisplayHelper
  12 +
  13 + def index
  14 + return unless load_page_tabs
  15 +
  16 + if params[:tabs_added]
  17 + @page_tabs = FbAppPlugin::PageTab.create_from_tabs_added params[:tabs_added]
  18 + @page_tab = @page_tabs.first
  19 + redirect_to @page_tab.facebook_url
  20 + elsif @signed_request or @page_id
  21 + if @page_tab.present?
  22 + if product_id = params[:product_id]
  23 + @product = environment.products.find product_id
  24 + @profile = @product.profile
  25 + @inputs = @product.inputs
  26 + @allowed_user = false
  27 + load_catalog
  28 +
  29 + render action: 'product'
  30 + elsif @page_tab.config_type.in? [:profile, :own_profile]
  31 + @profile = @page_tab.value
  32 +
  33 + load_catalog
  34 + render action: 'catalog' unless performed?
  35 + else
  36 + # fake profile for catalog controller
  37 + @profile = environment.enterprise_default_template
  38 + @profile.shopping_cart_settings.enabled = true
  39 +
  40 + base_query = @page_tab.value
  41 + params[:base_query] = base_query
  42 + params[:scope] = 'all'
  43 +
  44 + load_catalog
  45 + render action: 'catalog' unless performed?
  46 + end
  47 + else
  48 + render action: 'first_load'
  49 + end
  50 + else
  51 + # render template
  52 + render action: 'index'
  53 + end
  54 + end
  55 +
  56 + def search_autocomplete
  57 + load_page_tabs
  58 + load_search_autocomplete
  59 + respond_to do |format|
  60 + format.json{ render 'catalog/search_autocomplete' }
  61 + end
  62 + end
  63 +
  64 + def admin
  65 + return redirect_to '/plugin/fb_app/myprofile_config' if params[:page_id].blank? and params[:signed_request].blank?
  66 + return unless load_page_tabs
  67 +
  68 + if request.put? and @page_id.present?
  69 + create_page_tabs if @page_tab.nil?
  70 +
  71 + @page_tab.update_attributes! params[:page_tab]
  72 +
  73 + respond_to do |format|
  74 + format.js{ render action: 'admin' }
  75 + end
  76 + end
  77 + end
  78 +
  79 + def destroy
  80 + @page_tab = FbAppPlugin::PageTab.find params[:id]
  81 + return render_access_denied unless user.present? and (user.is_admin?(environment) or user.is_admin? @page_tab.profile)
  82 + @page_tab.destroy
  83 + render nothing: true
  84 + end
  85 +
  86 + def uninstall
  87 + render text: params.to_yaml
  88 + end
  89 +
  90 + def enterprise_search
  91 + scope = environment.enterprises.enabled.public
  92 + @query = params[:query]
  93 + @profiles = scope.limit(10).order('name ASC').
  94 + where(['name ILIKE ? OR name ILIKE ? OR identifier LIKE ?', "#{@query}%", "% #{@query}%", "#{@query}%"])
  95 + render partial: 'open_graph_plugin/myprofile/profile_search', locals: {profiles: @profiles}
  96 + end
  97 +
  98 + # unfortunetely, this needs to be public
  99 + def profile
  100 + @profile
  101 + end
  102 +
  103 + protected
  104 +
  105 + def default_url_options
  106 + {profile: @profile.identifier} if @profile
  107 + end
  108 +
  109 + def load_page_tabs
  110 + @signed_requests = read_param params[:signed_request]
  111 + if @signed_requests.present?
  112 + @datas = []
  113 + @page_ids = @signed_requests.map do |signed_request|
  114 + @data = FbAppPlugin::Auth.parse_signed_request signed_request
  115 + @datas << @data
  116 + page_id = @data[:page][:id] rescue nil
  117 + if page_id.blank?
  118 + render_not_found
  119 + return false
  120 + end
  121 + page_id
  122 + end
  123 + else
  124 + @page_ids = read_param params[:page_id]
  125 + end
  126 +
  127 + @page_tabs = FbAppPlugin::PageTab.where page_id: @page_ids
  128 +
  129 + @signed_request = @signed_requests.first
  130 + @page_id = @page_ids.first
  131 + @page_tab = @page_tabs.first
  132 + @new_request = @page_tab.blank?
  133 +
  134 + true
  135 + end
  136 +
  137 + def create_page_tabs
  138 + @page_tabs = FbAppPlugin::PageTab.create_from_page_ids @page_ids
  139 + @page_tab ||= @page_tabs.first
  140 + end
  141 +
  142 + def change_theme
  143 + # move to config
  144 + unless theme_responsive?
  145 + @current_theme = 'ees'
  146 + @theme_responsive = true
  147 + end
  148 + @without_pure_chat = true
  149 + end
  150 + def get_layout
  151 + return nil if request.format == :js or request.xhr?
  152 +
  153 + return 'application-responsive'
  154 + end
  155 +
  156 + def disable_cache
  157 + @disable_cache_theme_navigation = true
  158 + end
  159 +
  160 + def load_catalog options = {}
  161 + @use_show_more = true
  162 + catalog_load_index options
  163 + end
  164 +
  165 + def read_param param
  166 + if param.is_a? Hash
  167 + param.values
  168 + else
  169 + Array(param).select{ |p| p.present? }
  170 + end
  171 + end
  172 +
  173 +end
... ...
plugins/fb_app/db/migrate/20140319135819_create_fb_app_page_tab_config.rb 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +class CreateFbAppPageTabConfig < ActiveRecord::Migration
  2 +
  3 + def change
  4 + create_table :fb_app_plugin_page_tab_configs do |t|
  5 + t.string :page_id
  6 + t.text :config, default: {}.to_yaml
  7 + t.integer :profile_id
  8 +
  9 + t.timestamps
  10 + end
  11 + add_index :fb_app_plugin_page_tab_configs, [:profile_id]
  12 + add_index :fb_app_plugin_page_tab_configs, [:page_id]
  13 + add_index :fb_app_plugin_page_tab_configs, [:page_id, :profile_id]
  14 + end
  15 +
  16 +end
... ...
plugins/fb_app/install.rb 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +system "script/noosfero-plugins -q enable oauth_client"
  2 +system "script/noosfero-plugins -q enable open_graph"
  3 +
... ...
plugins/fb_app/lib/ext/profile.rb 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +require_dependency 'profile'
  2 +# hate to wrte this, but without Noosfero::Plugin::Settings is loaded instead
  3 +require 'fb_app_plugin/settings'
  4 +
  5 +# attr_accessible must be defined on subclasses
  6 +Profile.descendants.each do |subclass|
  7 + subclass.class_eval do
  8 + attr_accessible :fb_app_settings
  9 + end
  10 +end
  11 +
  12 +class Profile
  13 +
  14 + def fb_app_settings attrs = {}
  15 + @fb_app_settings ||= FbAppPlugin::Settings.new self, attrs
  16 + attrs.each{ |a, v| @fb_app_settings.send "#{a}=", v }
  17 + @fb_app_settings
  18 + end
  19 + alias_method :fb_app_settings=, :fb_app_settings
  20 +
  21 + has_many :fb_app_page_tabs, class_name: 'FbAppPlugin::PageTab'
  22 +
  23 + def fb_app_auth
  24 + provider = FbAppPlugin.oauth_provider_for self.environment
  25 + self.oauth_auths.where(provider_id: provider.id).first
  26 + end
  27 +
  28 +end
... ...
plugins/fb_app/lib/fb_app_plugin.rb 0 → 100644
... ... @@ -0,0 +1,87 @@
  1 +module FbAppPlugin
  2 +
  3 + extend Noosfero::Plugin::ParentMethods
  4 +
  5 + def self.plugin_name
  6 + I18n.t 'fb_app_plugin.lib.plugin.name'
  7 + end
  8 +
  9 + def self.plugin_description
  10 + I18n.t 'fb_app_plugin.lib.plugin.description'
  11 + end
  12 +
  13 + def self.config
  14 + @config ||= HashWithIndifferentAccess.new(YAML.load File.read("#{File.dirname __FILE__}/../config.yml")) rescue {}
  15 + end
  16 +
  17 + def self.test_users
  18 + @test_users ||= self.config[:test_users]
  19 + end
  20 + def self.test_user? user
  21 + user and (self.test_users.blank? or self.test_users.include? user.identifier)
  22 + end
  23 +
  24 + def self.debug? actor=nil
  25 + self.test_user? actor
  26 + end
  27 +
  28 + def self.scope user
  29 + if self.test_user? user then 'publish_actions' else '' end
  30 + end
  31 +
  32 + def self.oauth_provider_for environment
  33 + return unless self.config.present?
  34 +
  35 + @oauth_providers ||= {}
  36 + @oauth_providers[environment] ||= begin
  37 + app_id = self.timeline_app_credentials[:id].to_s
  38 + app_secret = self.timeline_app_credentials[:secret].to_s
  39 +
  40 + client = environment.oauth_providers.where(client_id: app_id).first
  41 + # attributes that may be changed by the user
  42 + client ||= OauthClientPlugin::Provider.new strategy: 'facebook',
  43 + name: 'FB App', site: 'https://facebook.com'
  44 +
  45 + # attributes that should not change
  46 + client.attributes = {
  47 + client_id: app_id, client_secret: app_secret,
  48 + environment_id: environment.id,
  49 + }
  50 + client.save! if client.changed?
  51 +
  52 + client
  53 + end
  54 + end
  55 +
  56 + def self.open_graph_config
  57 + return unless self.config.present?
  58 +
  59 + @open_graph_config ||= begin
  60 + key = if self.config[:timeline][:use_test_app] then :test_app else :app end
  61 + self.config[key][:open_graph]
  62 + end
  63 + end
  64 +
  65 + def self.credentials app = :app
  66 + return unless self.config.present?
  67 + {id: self.config[app][:id], secret: self.config[app][:secret]}
  68 + end
  69 +
  70 + def self.timeline_app_credentials
  71 + return unless self.config.present?
  72 + @timeline_app_credentials ||= begin
  73 + key = if self.config[:timeline][:use_test_app] then :test_app else :app end
  74 + self.credentials key
  75 + end
  76 + end
  77 +
  78 + def self.page_tab_app_credentials
  79 + return unless self.config.present?
  80 + @page_tab_app_credentials ||= begin
  81 + key = if self.config[:page_tab][:use_test_app] then :test_app else :app end
  82 + self.credentials key
  83 + end
  84 + end
  85 +
  86 +end
  87 +
... ...
plugins/fb_app/lib/fb_app_plugin/base.rb 0 → 100644
... ... @@ -0,0 +1,35 @@
  1 +class FbAppPlugin::Base < Noosfero::Plugin
  2 +
  3 + def stylesheet?
  4 + true
  5 + end
  6 +
  7 + def js_files
  8 + ['fb_app.js'].map{ |j| "javascripts/#{j}" }
  9 + end
  10 +
  11 + def head_ending
  12 + return unless FbAppPlugin.config.present?
  13 + lambda do
  14 + tag 'meta', property: 'fb:app_id', content: FbAppPlugin.config[:app][:id]
  15 + end
  16 + end
  17 +
  18 + def control_panel_buttons
  19 + return unless FbAppPlugin.config.present?
  20 + { title: FbAppPlugin.plugin_name, icon: 'fb-app', url: {host: FbAppPlugin.config[:app][:domain], profile: profile.identifier, controller: :fb_app_plugin_myprofile} }
  21 + end
  22 +
  23 +end
  24 +
  25 +ActiveSupport.on_load :open_graph_plugin do
  26 + OpenGraphPlugin::Stories.register_publisher FbAppPlugin::Publisher.default
  27 +end
  28 +ActiveSupport.on_load :metadata_plugin do
  29 + MetadataPlugin::Controllers.class_eval do
  30 + def fb_app_plugin_page_tab
  31 + :@product
  32 + end
  33 + end
  34 +end
  35 +
... ...
plugins/fb_app/lib/fb_app_plugin/display_helper.rb 0 → 100644
... ... @@ -0,0 +1,51 @@
  1 +module FbAppPlugin::DisplayHelper
  2 +
  3 + extend CatalogHelper
  4 +
  5 + def fb_url_options options
  6 + options.merge! page_id: @page_ids, signed_request: @signed_requests, id: nil
  7 + end
  8 +
  9 + def url_for options = {}
  10 + return super unless options.is_a? Hash
  11 + if options[:controller] == :catalog
  12 + options[:controller] = :fb_app_plugin_page_tab
  13 + options = fb_url_options options
  14 + end
  15 + super
  16 + end
  17 +
  18 + protected
  19 +
  20 + def product_url_options product, options = {}
  21 + options = options.merge! product.url
  22 + options = options.merge! controller: :fb_app_plugin_page_tab, product_id: product.id, action: :index
  23 + options = fb_url_options options
  24 + unless Rails.env.development?
  25 + domain = FbAppPlugin.config[:app][:domain]
  26 + options[:host] = domain if domain.present?
  27 + options[:protocol] = '//'
  28 + end
  29 + options
  30 + end
  31 + def product_path product, options = {}
  32 + url = url_for product_url_options(product, options = {})
  33 + url
  34 + end
  35 +
  36 + def link_to_product product, opts = {}
  37 + url_opts = opts.delete(:url_options) || {}
  38 + url_opts = product_url_options product, url_opts
  39 + url = params.merge url_opts
  40 + link_to content_tag('span', product.name), url,
  41 + opts.merge(target: '')
  42 + end
  43 +
  44 + def link_to name = nil, options = nil, html_options = nil, &block
  45 + html_options ||= {}
  46 + options[:protocol] = '//' if options.is_a? Hash
  47 + html_options[:target] ||= '_parent'
  48 + super
  49 + end
  50 +
  51 +end
... ...
plugins/fb_app/lib/fb_app_plugin/link_renderer.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +# add target attribute to links
  2 +class FbAppPlugin::LinkRenderer < WillPaginate::ActionView::LinkRenderer
  3 +
  4 + def prepare collection, options, template
  5 + super
  6 + end
  7 +
  8 + protected
  9 +
  10 + def default_url_params
  11 + {target: ''}
  12 + end
  13 +
  14 +end
... ...
plugins/fb_app/lib/fb_app_plugin/publisher.rb 0 → 100644
... ... @@ -0,0 +1,17 @@
  1 +# Publishing examples on console
  2 +# pub=FbAppPlugin::Publisher.default; u=Profile['brauliobo']; a=Article.find 307591
  3 +# pub.publish_story a, u, :announce_news_from_a_sse_initiative
  4 +#
  5 +# pub=FbAppPlugin::Publisher.default; u=Profile['brauliobo']; f=FavoriteEnterprisePerson.last
  6 +# pub.publish_story f, u, :favorite_a_sse_initiative
  7 +#
  8 +class FbAppPlugin::Publisher < OpenGraphPlugin::Publisher
  9 +
  10 + def publish_story object_data, actor, story
  11 + OpenGraphPlugin.context = FbAppPlugin::Activity.context
  12 + a = FbAppPlugin::Activity.new object_data: object_data, actor: actor, story: story
  13 + a.dispatch_publications
  14 + a.save
  15 + end
  16 +
  17 +end
... ...
plugins/fb_app/lib/fb_app_plugin/settings.rb 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +class FbAppPlugin::Settings < OpenGraphPlugin::Settings
  2 +
  3 +end
  4 +
... ...
plugins/fb_app/locales/en-US.yml 0 → 100644
... ... @@ -0,0 +1,83 @@
  1 +
  2 +"en-US": &en-US
  3 +
  4 + fb_app_plugin:
  5 + lib:
  6 + plugin:
  7 + name: 'Facebook integration'
  8 + description: 'Use the app for Facebook!'
  9 + models:
  10 + page_tab:
  11 + types:
  12 + own_profile: 'Catalog from this enterprise'
  13 + profile: 'Catalog from a single SSE enterprise'
  14 + other_profile: 'Catalog from other SSE enterprise'
  15 + profiles: 'Catalog from more than one SSE enterprise'
  16 + query: 'Catalog of products chosen by filter or free search'
  17 + views:
  18 + myprofile:
  19 + checking_auth: 'Checking authorization'
  20 + different_login: 'But you are logged in on facebook as:'
  21 + current_login: 'You are logged in on facebook as:'
  22 + connect: 'I want to install the app for Facebook'
  23 + logged_connect: 'Connect this Facebook account with my user %{profile}'
  24 + disconnect: 'Disconnect'
  25 + connect_to_another: 'Conect to another Facebook account'
  26 + reconnect: 'Reconnect'
  27 +
  28 + timeline:
  29 + heading: 'Publishing in your facebook'
  30 + add: 'Save timeline post settings'
  31 + explanation_title: 'Soon!'
  32 + explanation_text: 'In a short time, your actions will become new posts in your Facebook timeline!'
  33 + organization_redirect: '%{redirect_link} to post updates %{type}, conect your personal profile to Facebook.'
  34 + organization_from_enterprise: 'from this enterprise'
  35 + organization_from_community: 'from this community'
  36 + redirect_link: 'Click here'
  37 +
  38 + catalogs:
  39 + heading: 'Solidarity Economy Catalog'
  40 + new: 'Add new catalog'
  41 + catalog_title_label: 'Title'
  42 + catalog_subtitle_label: 'Subtitle'
  43 + catalog_type_chooser_label: 'Type'
  44 + profile_chooser_label: 'Enterprise'
  45 + profiles_chooser_label: 'Enterprises'
  46 + query_label: "Criteria for the catalog's products"
  47 + query_help: "Write the words separated by space, and click at the button below to save."
  48 + edit_button: "Edit catalog '%{catalog_title}'"
  49 + remove_button: "Remove catalog '%{catalog_title}'"
  50 + cancel_button: 'Cancel'
  51 + confirm_removal: "<p>Warning: To really remove a catalog, you must go to the facebook page where it is, click at '<u>add or remove tabs</u>' and then remove the tab.</p><p>After that, you can come here and remove the catalog from our settings.</p><p>Are you sure you want to remove this catalog?</p>"
  52 + confirm_removal_button: 'Yes, I want to delete this catalog'
  53 + confirm_disconnect: "<p>Warning: If you disconnect your account to this Facebook profile, all your catalogs will also be removed</p><p>And then, to really remove your catalogs (if they were created), you must go to the facebook page where it is, click at '<u>add or remove tabs</u>' and then remove the tab.</p><p>After that, you can come here and disconnect.</p><p>Are you sure you want to disconnect your account to this Facebook profile?</p>"
  54 + confirm_disconnect_button: 'Yes, I want to disconnect my account from this Facebook profile'
  55 +
  56 + catalog:
  57 + see_page: 'See page on Facebook'
  58 +
  59 + error:
  60 + empty_title: 'Please add a title to your catalog.'
  61 + empty_settings: 'Please choose SSE enterprises or search terms or filters for your catalog.'
  62 +
  63 + page_tab:
  64 + edit_catalog: 'Edit catalog'
  65 + add: 'Add catalog to one of my pages on Facebook'
  66 + save: 'Save'
  67 + added_notice: "Congratulations: You've just published a new SSE catalog in Facebook!"
  68 + profile:
  69 + placeholder: "select an enterprise"
  70 + profiles:
  71 + placeholder: "select the enterprises"
  72 + query:
  73 + placeholder: "select the category or type the search terms"
  74 + back_to_catalog: 'Back to catalog'
  75 + footer1: ""
  76 + footer2: ""
  77 +
  78 +
  79 +'en_US':
  80 + <<: *en-US
  81 +'en':
  82 + <<: *en-US
  83 +
... ...
plugins/fb_app/locales/pt-BR.yml 0 → 100644
... ... @@ -0,0 +1,82 @@
  1 +
  2 +"pt-BR": &pt-BR
  3 +
  4 + fb_app_plugin:
  5 + lib:
  6 + plugin:
  7 + name: 'App Facebook'
  8 + description: 'Divulgue suas ações no Facebook!'
  9 + models:
  10 + page_tab:
  11 + types:
  12 + own_profile: 'Vitrine deste empreendimento'
  13 + profile: 'Vitrine de um único empreendimento'
  14 + other_profile: 'Vitrine de outro empreendimento'
  15 + profiles: 'Vitrine de mais de um empreendimento'
  16 + query: 'Vitrine de produtos ou serviços escolhidos por busca livre'
  17 + views:
  18 + myprofile:
  19 + checking_auth: 'Verificando autorização de acesso'
  20 + different_login: 'Mas você está logado no facebook como:'
  21 + current_login: 'Você está logado no facebook como:'
  22 + connect: 'Quero instalar o App Facebook'
  23 + logged_connect: 'Quero conectar esta conta do Facebook com meu usuário %{profile}'
  24 + disconnect: 'Desconectar'
  25 + connect_to_another: 'Conectar a outra conta do Facebook'
  26 + reconnect: 'Reconectar'
  27 +
  28 + timeline:
  29 + heading: 'Postar ações no facebook automaticamente:'
  30 + add: 'Salvar configuração de postagens na timeline'
  31 + explanation_title: 'Aguarde!'
  32 + explanation_text: 'Em breve, este aplicativo poderá postar automaticamente no seu face as ações que você fizer! Por exemplo: quando você postar novos conteúdos no seu blog, enviar uma imagem, etc.'
  33 + organization_redirect: '%{redirect_link} para postar atualizações %{type}, conecte o seu perfil pessoal ao facebook.'
  34 + organization_from_enterprise: 'deste empreendimento'
  35 + organization_from_community: 'desta comunidade'
  36 + redirect_link: 'Clique aqui'
  37 +
  38 + catalogs:
  39 + heading: 'Vitrine'
  40 + new: 'Criar nova vitrine'
  41 + catalog_title_label: 'Título'
  42 + catalog_subtitle_label: 'Subtítulo'
  43 + catalog_type_chooser_label: 'Tipo'
  44 + profile_chooser_label: 'Empreendimento'
  45 + profiles_chooser_label: 'Empreendimentos'
  46 + query_label: "Critérios para os produtos/serviços da vitrine"
  47 + query_help: "Escreva as palavras separadas por espaço, e clique no botão abaixo para salvar."
  48 + edit_button: "Editar vitrine '%{catalog_title}'"
  49 + remove_button: "Remover vitrine '%{catalog_title}'"
  50 + cancel_button: 'Cancelar'
  51 + confirm_removal: "<p><b>Atenção:</b> Para realmente apagar uma vitrine, você deve primeiro ir para a página do Facebook onde está a sua vitrine e seguir este roteiro:</p><ul><li>Clique em '<i>Mais</i>' na barra da página, depois em '<i>Gerenciar guias</i>', e então em '<i>Adicionar ou remover guias</i>'.</li><li>. Clique no X para remover.</li></ul><p>Depois disso, você pode vir aqui e pedir para remover a vitrine.</p><p>Se você já removeu lá no Facebook, tem certeza que quer remover esta vitrine?</p>"
  52 + confirm_removal_button: 'Sim, quero remover esta vitrine'
  53 + confirm_disconnect: "<p><b>Atenção:</b> Se você desconectar, vai apagar todas as vitrines. Portanto, para realmente apagar suas vitrines, você deve primeiro ir para a página do Facebook onde está a sua vitrine e seguir este roteiro:</p><ul><li>Clique em '<i>Mais</i>' na barra da página, depois em '<i>Gerenciar guias</i>', e então em '<i>Adicionar ou remover guias</i>'.</li><li>Clique no X para remover.</li></ul><p>Depois disso, você pode vir aqui e desconectar.</p><p>Tem certeza que quer desconectar?</p>"
  54 + confirm_disconnect_button: 'Sim, quero desconectar'
  55 +
  56 + catalog:
  57 + see_page: 'Ver página no facebook'
  58 +
  59 + error:
  60 + empty_title: 'Por favor, coloque um título para a sua vitrine.'
  61 + empty_settings: 'Por favor, selecione os empreendimentos solidários ou os termos de busca para sua vitrine.'
  62 +
  63 + page_tab:
  64 + edit_catalog: 'Editar vitrine'
  65 + add: 'Criar vitrine em uma página sua no Facebook'
  66 + save: 'Salvar'
  67 + added_notice: 'Parabéns: você acaba de publicar uma nova vitrine da Economia Solidária no Facebook!'
  68 + profile:
  69 + placeholder: "selecione um empreendimento"
  70 + profiles:
  71 + placeholder: "selecione os empreendimentos"
  72 + query:
  73 + placeholder: "escolha os produtos e serviços por palavras de busca"
  74 + back_to_catalog: 'Voltar à vitrine'
  75 + footer1: ""
  76 + footer2: ""
  77 +
  78 +'pt_BR':
  79 + <<: *pt-BR
  80 +'pt':
  81 + <<: *pt-BR
  82 +
... ...
plugins/fb_app/models/fb_app_plugin/activity.rb 0 → 100644
... ... @@ -0,0 +1,54 @@
  1 +class FbAppPlugin::Activity < OpenGraphPlugin::Activity
  2 +
  3 + self.context = :fb_app
  4 + self.actions = FbAppPlugin.open_graph_config[:actions]
  5 + self.objects = FbAppPlugin.open_graph_config[:objects]
  6 +
  7 + # this avoid to many saves for frequent fail cases
  8 + attr_accessor :should_save
  9 + validates_presence_of :should_save
  10 +
  11 + def self.scrape object_data_url
  12 + params = {id: object_data_url, scrape: true, method: 'post'}
  13 + url = "http://graph.facebook.com?#{params.to_query}"
  14 + Net::HTTP.get URI.parse(url)
  15 + end
  16 + def scrape
  17 + self.class.scrape self.object_data_url
  18 + end
  19 +
  20 + def publish! actor = self.actor
  21 + print_debug "fb_app: action #{self.action}, object_type #{self.object_type}" if debug? actor
  22 +
  23 + auth = actor.fb_app_auth
  24 + return if auth.blank? or auth.expired?
  25 + print_debug "fb_app: Auth found and is valid" if debug? actor
  26 +
  27 + # always update the object to expire facebook cache
  28 + Thread.new{ self.scrape }
  29 +
  30 + return if self.defs[:on] == :update and self.recent_publish? actor, self.object_type, self.object_data_url
  31 + print_debug "fb_app: no recent publication found, making new" if debug? actor
  32 +
  33 + self.should_save = true
  34 +
  35 + namespace = FbAppPlugin.open_graph_config[:namespace]
  36 + # to_str is needed to ensure String, see https://github.com/nov/fb_graph2/issues/88
  37 + params = {self.object_type => self.object_data_url.to_str}
  38 + params['fb:explicitly_shared'] = 'true' unless self.defs[:tracker]
  39 + print_debug "fb_app: publishing with params #{params.inspect}" if debug? actor
  40 +
  41 + me = FbGraph2::User.me auth.access_token
  42 + me.og_action! "#{namespace}:#{action}", params
  43 +
  44 + self.published_at = Time.now
  45 + print_debug "fb_app: published with success" if debug? actor
  46 + end
  47 +
  48 + protected
  49 +
  50 + def debug? actor=nil
  51 + super or FbAppPlugin.debug? actor
  52 + end
  53 +
  54 +end
... ...
plugins/fb_app/models/fb_app_plugin/auth.rb 0 → 100644
... ... @@ -0,0 +1,89 @@
  1 +class FbAppPlugin::Auth < OauthClientPlugin::Auth
  2 +
  3 + module Status
  4 + Connected = 'connected'
  5 + NotAuthorized = 'not_authorized'
  6 + Unknown = 'unknown'
  7 + end
  8 +
  9 + settings_items :signed_request
  10 + settings_items :fb_user
  11 +
  12 + attr_accessible :provider_user_id, :signed_request
  13 +
  14 + before_create :update_user
  15 + before_create :exchange_token
  16 + after_create :schedule_exchange_token
  17 + after_destroy :destroy_page_tabs
  18 + before_validation :set_enabled
  19 +
  20 + validates_presence_of :provider_user_id
  21 + validates_uniqueness_of :provider_user_id, scope: :profile_id
  22 +
  23 + def self.parse_signed_request signed_request, credentials = FbAppPlugin.page_tab_app_credentials
  24 + secret = credentials[:secret] rescue ''
  25 + request = Facebook::SignedRequest.new signed_request, secret: secret
  26 + request.data
  27 + end
  28 +
  29 + def status
  30 + if self.access_token.present? and self.not_expired? then Status::Connected else Status::NotAuthorized end
  31 + end
  32 + def not_authorized?
  33 + self.status == Status::NotAuthorized
  34 + end
  35 + def connected?
  36 + self.status == Status::Connected
  37 + end
  38 +
  39 + def exchange_token
  40 + app_id = FbAppPlugin.timeline_app_credentials[:id]
  41 + app_secret = FbAppPlugin.timeline_app_credentials[:secret]
  42 + fb_auth = FbGraph2::Auth.new app_id, app_secret
  43 + fb_auth.fb_exchange_token = self.access_token
  44 +
  45 + access_token = fb_auth.access_token!
  46 + self.access_token = access_token.access_token
  47 + self.expires_in = access_token.expires_in
  48 + # refresh user and its stored access token
  49 + self.fetch_user
  50 + end
  51 +
  52 + def exchange_token!
  53 + self.exchange_token
  54 + self.save!
  55 + end
  56 +
  57 + def signed_request_data
  58 + self.class.parse_signed_request self.signed_request
  59 + end
  60 +
  61 + def fetch_user
  62 + fb_user = FbGraph2::User.me self.access_token
  63 + self.fb_user = fb_user.fetch
  64 + end
  65 + def update_user
  66 + self.fb_user = self.fetch_user
  67 + end
  68 +
  69 + protected
  70 +
  71 + def destroy_page_tabs
  72 + self.profile.fb_app_page_tabs.destroy_all
  73 + end
  74 +
  75 + def exchange_token_and_reschedule!
  76 + self.exchange_token!
  77 + self.schedule_exchange_token
  78 + end
  79 +
  80 + def schedule_exchange_token
  81 + self.delay(run_at: self.expires_at - 2.weeks).exchange_token_and_reschedule!
  82 + end
  83 +
  84 + def set_enabled
  85 + self.enabled = self.not_expired?
  86 + end
  87 +
  88 +end
  89 +
... ...
plugins/fb_app/models/fb_app_plugin/page_tab.rb 0 → 100644
... ... @@ -0,0 +1,111 @@
  1 +class FbAppPlugin::PageTab < ActiveRecord::Base
  2 +
  3 + # FIXME: rename table to match model
  4 + self.table_name = :fb_app_plugin_page_tab_configs
  5 +
  6 + attr_accessible :owner_profile, :profile_id, :page_id,
  7 + :config_type, :profile_ids, :query,
  8 + :title, :subtitle
  9 +
  10 + belongs_to :owner_profile, foreign_key: :profile_id, class_name: 'Profile'
  11 +
  12 + acts_as_having_settings field: :config
  13 +
  14 + ConfigTypes = [:profile, :profiles, :query]
  15 + EnterpriseConfigTypes = [:own_profile] + ConfigTypes
  16 +
  17 + validates_presence_of :page_id
  18 + validates_uniqueness_of :page_id
  19 + validates_inclusion_of :config_type, in: ConfigTypes + EnterpriseConfigTypes
  20 +
  21 + def self.page_ids_from_tabs_added tabs_added
  22 + tabs_added.map{ |id, value| id }
  23 + end
  24 +
  25 + def self.create_from_page_ids page_ids, attrs = {}
  26 + attrs.delete :page_id
  27 + page_ids.map do |page_id|
  28 + page_tab = FbAppPlugin::PageTab.where(page_id: page_id).first
  29 + page_tab ||= FbAppPlugin::PageTab.new page_id: page_id
  30 + page_tab.update_attributes! attrs
  31 + page_tab
  32 + end
  33 + end
  34 + def self.create_from_tabs_added tabs_added, attrs = {}
  35 + page_ids = self.page_ids_from_tabs_added tabs_added
  36 + self.create_from_page_ids page_ids, attrs
  37 + end
  38 +
  39 + def self.facebook_url page_id
  40 + "https://facebook.com/#{page_id}?sk=app_#{FbAppPlugin.page_tab_app_credentials[:id]}"
  41 + end
  42 +
  43 + def facebook_url
  44 + self.class.facebook_url self.page_id
  45 + end
  46 +
  47 + def types
  48 + if self.owner_profile.present? and self.owner_profile.enterprise? then EnterpriseConfigTypes else ConfigTypes end
  49 + end
  50 +
  51 + def config_type
  52 + self.config[:type] || (self.owner_profile ? :own_profile : :profile)
  53 + end
  54 + def config_type= value
  55 + self.config[:type] = value.to_sym
  56 + end
  57 +
  58 + def value
  59 + case self.config_type
  60 + when :profiles
  61 + self.profiles.map(&:identifier).join(' OR ')
  62 + else
  63 + self.send self.config_type
  64 + end
  65 + end
  66 + def blank?
  67 + self.value.blank? rescue true
  68 + end
  69 +
  70 + def own_profile
  71 + self.owner_profile
  72 + end
  73 + def profiles
  74 + Profile.where(id: self.config[:profile_ids])
  75 + end
  76 + def profile
  77 + self.profiles.first
  78 + end
  79 + def profile_ids
  80 + self.profiles.map(&:id)
  81 + end
  82 + def query
  83 + self.config[:query]
  84 + end
  85 +
  86 + def title
  87 + self.config[:title]
  88 + end
  89 + def title= value
  90 + self.config[:title] = value
  91 + end
  92 +
  93 + def subtitle
  94 + self.config[:subtitle]
  95 + end
  96 + def subtitle= value
  97 + self.config[:subtitle] = value
  98 + end
  99 +
  100 + def profile_ids= ids
  101 + ids = ids.to_s.split(',')
  102 + self.config[:type] = if ids.size == 1 then :profile else :profiles end
  103 + self.config[:profile_ids] = ids
  104 + end
  105 +
  106 + def query= value
  107 + self.config[:type] = :query
  108 + self.config[:query] = value
  109 + end
  110 +
  111 +end
... ...
plugins/fb_app/plugins/fb_app/lib/ext/action_tracker_model.rb 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +require_dependency 'action_tracker_model'
  2 +
  3 +class ActionTracker::Record
  4 +
  5 + after_create :fb_app_publish
  6 +
  7 + protected
  8 +
  9 + def fb_app_publish
  10 + raise 'here'
  11 + end
  12 +end
... ...
plugins/fb_app/public/images/FB-f-Logo__blue_48.png 0 → 100644

722 Bytes

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

3.31 KB

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

18.3 KB

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