Commit c70a99e99358023a5b07efa84bd7756c5ab3f5e3

Authored by Leandro Santos
2 parents 6c96c297 c5b5a296

Merge branch 'master' into AI3205-comment-paragraph

Showing 1052 changed files with 54048 additions and 21919 deletions   Show diff stats

Too many changes.

To preserve performance only 100 of 1052 files displayed.

Gemfile
1 1 source "https://rubygems.org"
2   -gem 'rails', '~> 3.2.19'
  2 +gem 'rails', '~> 3.2.21'
  3 +gem 'minitest', '~> 3.2.0'
3 4 gem 'fast_gettext', '~> 0.6.8'
4   -gem 'acts-as-taggable-on', '~> 3.0.2'
5   -gem 'prototype-rails', '~> 3.2.1'
6   -gem 'prototype_legacy_helper', '0.0.0', :path => 'vendor/prototype_legacy_helper'
  5 +gem 'acts-as-taggable-on', '~> 3.4.2'
7 6 gem 'rails_autolink', '~> 1.1.5'
8 7 gem 'pg', '~> 0.13.2'
9 8 gem 'rmagick', '~> 2.13.1'
... ... @@ -12,16 +11,14 @@ gem 'will_paginate', '~> 3.0.3'
12 11 gem 'ruby-feedparser', '~> 0.7'
13 12 gem 'daemons', '~> 1.1.5'
14 13 gem 'thin', '~> 1.3.1'
15   -gem 'hpricot', '~> 0.8.6'
16 14 gem 'nokogiri', '~> 1.5.5'
17 15 gem 'rake', :require => false
18 16 gem 'rest-client', '~> 1.6.7'
19 17 gem 'exception_notification', '~> 4.0.1'
20 18 gem 'gettext', '~> 2.2.1', :require => false, :group => :development
21 19 gem 'locale', '~> 2.0.5'
22   -
23   -# FIXME list here all actual dependencies (i.e. the ones in debian/control),
24   -# with their GEM names (not the Debian package names)
  20 +gem 'whenever', :require => false
  21 +gem 'eita-jrails', '>= 0.9.5', :require => 'jrails'
25 22  
26 23 group :production do
27 24 gem 'dalli', '~> 2.7.0'
... ... @@ -41,6 +38,9 @@ group :cucumber do
41 38 gem 'selenium-webdriver', '~> 2.39.0'
42 39 end
43 40  
  41 +# Requires custom dependencies
  42 +eval(File.read('config/Gemfile'), binding) rescue nil
  43 +
44 44 # include gemfiles from enabled plugins
45 45 # plugins in baseplugins/ are not included on purpose. They should not have any
46 46 # dependencies.
... ...
INSTALL.md
... ... @@ -21,7 +21,7 @@ Noosfero is written in Ruby with the "[Rails framework](http://www.rubyonrails.o
21 21 You need to install some packages Noosfero depends on. On Debian GNU/Linux or Debian-based systems, all of these packages are available through the Debian archive. You can install them with the following command:
22 22  
23 23 # apt-get install ruby rake po4a libgettext-ruby-util libgettext-ruby1.8 \
24   - libsqlite3-ruby rcov librmagick-ruby libredcloth-ruby libhpricot-ruby \
  24 + libsqlite3-ruby rcov librmagick-ruby libredcloth-ruby \
25 25 libwill-paginate-ruby iso-codes libfeedparser-ruby libdaemons-ruby thin \
26 26 tango-icon-theme
27 27  
... ... @@ -40,7 +40,6 @@ On other systems, they may or may not be available through your regular package
40 40 * Daemons - http://daemons.rubyforge.org
41 41 * Thin: http://code.macournoyer.com/thin
42 42 * tango-icon-theme: http://tango.freedesktop.org/Tango_Icon_Library
43   -* Hpricot: http://hpricot.com
44 43  
45 44 If you manage to install Noosfero successfully on other systems than Debian,
46 45 please feel free to contact the Noosfero development mailing with the
... ...
INSTALL.multitenancy.md
... ... @@ -26,7 +26,7 @@ The file config/database.yml must follow a structure in order to achieve multite
26 26  
27 27 Each "hosted" environment must have an entry like this:
28 28  
29   - env1_production:
  29 + env1_production: &DEFAULT
30 30 adapter: postgresql
31 31 encoding: unicode
32 32 database: noosfero
... ... @@ -61,7 +61,7 @@ The "hosted" environments define, besides the `schema_search_path`, a list of do
61 61 You must also tell the application which is the default environment.
62 62  
63 63 production:
64   - env1_production
  64 + <<: *DEFAULT
65 65  
66 66 On the example above there are only three hosted environments, but it can be more than three. The schemas `env2` and `env3` must already exist in the same database of the hosting environment. As postgres user, you can create them typing:
67 67  
... ...
MIGRATION_ISSUES
... ... @@ -1,41 +0,0 @@
1   -* ruby-get-text incmopatible with rails3. Maybe we can use it's gem
2   -
3   -* all js code is inside miscellaneous.js. Would be nice to refactor this
4   -
5   -* rails 2 uses prototype instead of jquery
6   -
7   -* config/environment.rb maybe still have some code that should be on the initializers
8   -
9   -* initializers session_store.rb inflections.rb... don't exist
10   -
11   -* rails gems version have to be forced on Gemfile or it will use incompatible pre3vious versions (3.1.3)
12   -
13   -* Sweepers are now natively supported on Rails 3. Would be nice to refactor it
14   -
15   -* On Rails 3 it is no more possible to add allowed tags to avoid scape. The html_safe initializer is an option.
16   -
17   -* error when call sqlite_extensiosn
18   -
19   -* error related to action_tracker
20   -
21   -* check FIXME's in script/quick-start
22   -
23   -* check FIXME's in Gemfile
24   -
25   -* Check the FIXME in config/routes.rb
26   -
27   -* rewrite conditional routing. See FIXME in lib/route_if.rb and re-implement using the Rails 3 mechanism - http://guides.rubyonrails.org/routing.html#advanced-constraints
28   -
29   -* check FIXME's in config/environment.rb
30   -
31   -* xss_terminate sucks. We should replace it with the builtin mechanism in Rails 3
32   -
33   -* instance_eval on Ruby 1.9 yields self, so lambdas that are passed to instance_eval and do not accept exactly 1 argument will blow up. See http://www.ruby-forum.com/topic/213313 ... search for instance_eval and fix where necessary. In special, most of the blocks still need fixing.
34   -
35   -* all instances of <% *_form_for ... %> must be changed to <%= instead of <%
36   -
37   -* all ActiveRecord models have to declare explicitly which attributes must be allowed for mass assignment with attr_accessible.
38   -
39   -* check if we need to update config/locales/*
40   -
41   -* check observe_field and labelled_form_for in app/helpers/application_helper.rb
SECURITY.md 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +# Noosfero security
  2 +
  3 +## Reporting security issues in Noosfero
  4 +
  5 +Security vulnerabilities should be reported via an email to
  6 +noosfero-security@listas.softwarelivre.org, which ia a private mailing list.
  7 +Reported problems will be published after fixes are available.
  8 +
  9 +The members of the mailing list are people who provide Noosfero (Noosfero
  10 +committers, mainly).
... ...
app/controllers/admin/admin_panel_controller.rb
... ... @@ -87,6 +87,6 @@ class AdminPanelController &lt; AdminController
87 87 scope = scope.order('name ASC')
88 88  
89 89 @q = params[:q]
90   - @collection = find_by_contents(:organizations, scope, @q, {:per_page => 10, :page => params[:npage]})[:results]
  90 + @collection = find_by_contents(:organizations, environment, scope, @q, {:per_page => 10, :page => params[:npage]})[:results]
91 91 end
92 92 end
... ...
app/controllers/admin/region_validators_controller.rb
... ... @@ -33,7 +33,7 @@ class RegionValidatorsController &lt; AdminController
33 33 def load_region_and_search
34 34 @region = environment.regions.find(params[:id])
35 35 if params[:search]
36   - @search = find_by_contents(:organizations, Organization, params[:search])[:results].reject {|item| @region.validator_ids.include?(item.id) }
  36 + @search = find_by_contents(:organizations, environment, Organization, params[:search])[:results].reject {|item| @region.validator_ids.include?(item.id) }
37 37 end
38 38 end
39 39  
... ...
app/controllers/admin/users_controller.rb
... ... @@ -18,7 +18,7 @@ class UsersController &lt; AdminController
18 18 end
19 19 scope = scope.order('name ASC')
20 20 @q = params[:q]
21   - @collection = find_by_contents(:people, scope, @q, {:per_page => per_page, :page => params[:npage]})[:results]
  21 + @collection = find_by_contents(:people, environment, scope, @q, {:per_page => per_page, :page => params[:npage]})[:results]
22 22 end
23 23  
24 24 def set_admin_role
... ...
app/controllers/application_controller.rb
... ... @@ -28,6 +28,7 @@ class ApplicationController &lt; ActionController::Base
28 28 unless environment.access_control_allow_methods.blank?
29 29 response.headers["Access-Control-Allow-Methods"] = environment.access_control_allow_methods
30 30 end
  31 + response.headers["Access-Control-Allow-Credentials"] = 'true'
31 32 elsif environment.restrict_to_access_control_origins
32 33 render_access_denied _('Origin not in allowed.')
33 34 end
... ... @@ -59,15 +60,7 @@ class ApplicationController &lt; ActionController::Base
59 60 helper :document
60 61 helper :language
61 62  
62   - def self.no_design_blocks
63   - @no_design_blocks = true
64   - end
65   - def self.uses_design_blocks?
66   - !@no_design_blocks
67   - end
68   - def uses_design_blocks?
69   - !@no_design_blocks && self.class.uses_design_blocks?
70   - end
  63 + include DesignHelper
71 64  
72 65 # Be sure to include AuthenticationSystem in Application Controller instead
73 66 include AuthenticatedSystem
... ... @@ -183,21 +176,19 @@ class ApplicationController &lt; ActionController::Base
183 176 end
184 177 end
185 178  
186   - def find_by_contents(asset, scope, query, paginate_options={:page => 1}, options={})
187   - plugins.dispatch_first(:find_by_contents, asset, scope, query, paginate_options, options) ||
188   - fallback_find_by_contents(asset, scope, query, paginate_options, options)
189   - end
  179 + include SearchTermHelper
190 180  
191   - private
  181 + def find_by_contents(asset, context, scope, query, paginate_options={:page => 1}, options={})
  182 + search = plugins.dispatch_first(:find_by_contents, asset, scope, query, paginate_options, options)
  183 + register_search_term(query, scope.count, search[:results].count, context, asset)
  184 + search
  185 + end
192 186  
193   - def fallback_find_by_contents(asset, scope, query, paginate_options, options)
194   - scope = scope.like_search(query) unless query.blank?
195   - scope = scope.send(options[:filter]) unless options[:filter].blank?
196   - {:results => scope.paginate(paginate_options)}
  187 + def find_suggestions(query, context, asset, options={})
  188 + plugins.dispatch_first(:find_suggestions, query, context, asset, options)
197 189 end
198 190  
199 191 def private_environment?
200 192 @environment.enabled?(:restrict_to_members)
201 193 end
202   -
203 194 end
... ...
app/controllers/my_profile/cms_controller.rb
... ... @@ -23,6 +23,9 @@ class CmsController &lt; MyProfileController
23 23 end
24 24  
25 25 before_filter :login_required, :except => [:suggest_an_article]
  26 + before_filter :load_recent_files, :only => [:new, :edit]
  27 +
  28 + helper_method :file_types
26 29  
27 30 protect_if :only => :upload_files do |c, user, profile|
28 31 article_id = c.params[:parent_id]
... ... @@ -30,7 +33,7 @@ class CmsController &lt; MyProfileController
30 33 (user && (user.has_permission?('post_content', profile) || user.has_permission?('publish_content', profile)))
31 34 end
32 35  
33   - protect_if :except => [:suggest_an_article, :set_home_page, :edit, :destroy, :publish, :upload_files, :new] do |c, user, profile|
  36 + protect_if :except => [:suggest_an_article, :set_home_page, :edit, :destroy, :publish, :publish_on_portal_community, :publish_on_communities, :search_communities_to_publish, :upload_files, :new] do |c, user, profile|
34 37 user && (user.has_permission?('post_content', profile) || user.has_permission?('publish_content', profile))
35 38 end
36 39  
... ... @@ -40,7 +43,7 @@ class CmsController &lt; MyProfileController
40 43 (user && (user.has_permission?('post_content', profile) || user.has_permission?('publish_content', profile)))
41 44 end
42 45  
43   - protect_if :only => [:destroy, :publish] do |c, user, profile|
  46 + protect_if :only => :destroy do |c, user, profile|
44 47 profile.articles.find(c.params[:id]).allow_post_content?(user)
45 48 end
46 49  
... ... @@ -117,7 +120,7 @@ class CmsController &lt; MyProfileController
117 120 @success_back_to = params[:success_back_to]
118 121 # user must choose an article type first
119 122  
120   - @parent = profile.articles.find(params[:parent_id]) if params && params[:parent_id]
  123 + @parent = profile.articles.find(params[:parent_id]) if params && params[:parent_id].present?
121 124 record_coming
122 125 @type = params[:type]
123 126 if @type.blank?
... ... @@ -163,7 +166,10 @@ class CmsController &lt; MyProfileController
163 166 if continue
164 167 redirect_to :action => 'edit', :id => @article
165 168 else
166   - success_redirect
  169 + respond_to do |format|
  170 + format.html { success_redirect }
  171 + format.json { render :text => {:id => @article.id, :full_name => profile.identifier + '/' + @article.full_name}.to_json }
  172 + end
167 173 end
168 174 return
169 175 end
... ... @@ -256,28 +262,53 @@ class CmsController &lt; MyProfileController
256 262 render :template => 'shared/update_categories', :locals => { :category => @current_category, :object_name => 'article' }
257 263 end
258 264  
  265 + def search_communities_to_publish
  266 + render :text => find_by_contents(:profiles, environment, user.memberships, params['q'], {:page => 1}, {:fields => ['name']})[:results].map {|community| {:id => community.id, :name => community.name} }.to_json
  267 + end
  268 +
259 269 def publish
260 270 @article = profile.articles.find(params[:id])
261 271 record_coming
262   - @groups = profile.memberships - [profile]
263   - @marked_groups = []
264   - groups_ids = profile.memberships.map{|m|m.id.to_s}
265   - @marked_groups = params[:marked_groups].map do |key, item|
266   - if groups_ids.include?(item[:group_id])
267   - item.merge :group => Profile.find(item.delete(:group_id))
  272 + @failed = {}
  273 + if request.post?
  274 + article_name = params[:name]
  275 + task = ApproveArticle.create!(:article => @article, :name => article_name, :target => user, :requestor => user)
  276 + begin
  277 + task.finish
  278 + rescue Exception => ex
  279 + @failed[ex.message] ? @failed[ex.message] << @article.name : @failed[ex.message] = [@article.name]
  280 + task.cancel
  281 + end
  282 + if @failed.blank?
  283 + session[:notice] = _("Your publish request was sent successfully")
  284 + if @back_to
  285 + redirect_to @back_to
  286 + else
  287 + redirect_to @article.view_url
  288 + end
268 289 end
269   - end.compact unless params[:marked_groups].nil?
  290 + end
  291 + end
  292 +
  293 + def publish_on_communities
270 294 if request.post?
  295 + @back_to = params[:back_to]
  296 + @article = profile.articles.find(params[:id])
271 297 @failed = {}
  298 + article_name = params[:name]
  299 + params_marked = (params['q'] || '').split(',').select { |marked| user.memberships.map(&:id).include? marked.to_i }
  300 + @marked_groups = Profile.find(params_marked)
272 301 if @marked_groups.empty?
  302 + redirect_to @back_to
273 303 return session[:notice] = _("Select some group to publish your article")
274 304 end
275 305 @marked_groups.each do |item|
276   - task = ApproveArticle.create!(:article => @article, :name => item[:name], :target => item[:group], :requestor => profile)
  306 + task = ApproveArticle.create!(:article => @article, :name => article_name, :target => item, :requestor => user)
277 307 begin
278   - task.finish unless item[:group].moderated_articles?
  308 + task.finish unless item.moderated_articles?
279 309 rescue Exception => ex
280   - @failed[ex.message] ? @failed[ex.message] << item[:group].name : @failed[ex.message] = [item[:group].name]
  310 + @failed[ex.message] ? @failed[ex.message] << item.name : @failed[ex.message] = [item.name]
  311 + task.cancel
281 312 end
282 313 end
283 314 if @failed.blank?
... ... @@ -287,23 +318,27 @@ class CmsController &lt; MyProfileController
287 318 else
288 319 redirect_to @article.view_url
289 320 end
  321 + else
  322 + session[:notice] = _("Some of your publish requests couldn't be sent.")
  323 + render :action => 'publish'
290 324 end
291 325 end
292 326 end
293 327  
294 328 def publish_on_portal_community
295   - @article = profile.articles.find(params[:id])
296 329 if request.post?
297   - if environment.portal_community
  330 + @article = profile.articles.find(params[:id])
  331 + if environment.portal_enabled
298 332 task = ApproveArticle.create!(:article => @article, :name => params[:name], :target => environment.portal_community, :requestor => user)
299 333 begin
300 334 task.finish unless environment.portal_community.moderated_articles?
301   - flash[:notice] = _("Your publish request was sent successfully")
  335 + session[:notice] = _("Your publish request was sent successfully")
302 336 rescue
303   - flash[:error] = _("Your publish request couldn't be sent.")
  337 + session[:notice] = _("Your publish request couldn't be sent.")
  338 + task.cancel
304 339 end
305 340 else
306   - flash[:notice] = _("There is no portal community to publish your article.")
  341 + session[:notice] = _("There is no portal community to publish your article.")
307 342 end
308 343  
309 344 if @back_to
... ... @@ -331,7 +366,7 @@ class CmsController &lt; MyProfileController
331 366  
332 367 def search
333 368 query = params[:q]
334   - results = find_by_contents(:uploaded_files, profile.files.published, query)[:results]
  369 + results = find_by_contents(:uploaded_files, profile, profile.files.published, query)[:results]
335 370 render :text => article_list_to_json(results), :content_type => 'application/json'
336 371 end
337 372  
... ... @@ -342,15 +377,26 @@ class CmsController &lt; MyProfileController
342 377 end
343 378  
344 379 def media_upload
345   - files_uploaded = []
346 380 parent = check_parent(params[:parent_id])
347   - files = [:file1,:file2, :file3].map { |f| params[f] }.compact
348 381 if request.post?
349   - files.each do |file|
350   - files_uploaded << UploadedFile.create(:uploaded_data => file, :profile => profile, :parent => parent) unless file == ''
  382 + begin
  383 + @file = UploadedFile.create!(:uploaded_data => params[:file], :profile => profile, :parent => parent) unless params[:file] == ''
  384 + @file = FilePresenter.for(@file)
  385 + rescue Exception => exception
  386 + render :text => exception.to_s, :status => :bad_request
351 387 end
352 388 end
353   - render :text => article_list_to_json(files_uploaded), :content_type => 'text/plain'
  389 + end
  390 +
  391 + def published_media_items
  392 + load_recent_files(params[:parent_id], params[:q])
  393 + render :partial => 'published_media_items'
  394 + end
  395 +
  396 + def view_all_media
  397 + paginate_options = {:page => params[:page].blank? ? 1 : params[:page] }
  398 + @key = params[:key].to_sym
  399 + load_recent_files(params[:parent_id], params[:q], paginate_options)
354 400 end
355 401  
356 402 protected
... ... @@ -445,4 +491,36 @@ class CmsController &lt; MyProfileController
445 491 end
446 492 end
447 493  
  494 + def file_types
  495 + {:images => _('Images'), :generics => _('Files')}
  496 + end
  497 +
  498 + def load_recent_files(parent_id = nil, q = nil, paginate_options = {:page => 1, :per_page => 6})
  499 + #TODO Since we only have special support for images, I'm limiting myself to
  500 + # consider generic files as non-images. In the future, with more supported
  501 + # file types we'll need to have a smart way to fetch from the database
  502 + # scopes of each supported type as well as the non-supported types as a
  503 + # whole.
  504 + @recent_files = {}
  505 +
  506 + parent = parent_id.present? ? profile.articles.find(parent_id) : nil
  507 + if parent.present?
  508 + files = parent.children.files
  509 + else
  510 + files = profile.files
  511 + end
  512 +
  513 + files = files.reorder('created_at DESC')
  514 + images = files.images
  515 + generics = files.no_images
  516 +
  517 + if q.present?
  518 + @recent_files[:images] = find_by_contents(:images, profile, images, q, paginate_options)[:results]
  519 + @recent_files[:generics] = find_by_contents(:generics, profile, generics, q, paginate_options)[:results]
  520 + else
  521 + @recent_files[:images] = images.paginate(paginate_options)
  522 + @recent_files[:generics] = generics.paginate(paginate_options)
  523 + end
  524 + end
  525 +
448 526 end
... ...
app/controllers/my_profile/friends_controller.rb
... ... @@ -3,6 +3,7 @@ class FriendsController &lt; MyProfileController
3 3 protect 'manage_friends', :profile
4 4  
5 5 def index
  6 + @suggestions = profile.profile_suggestions.of_person.enabled.includes(:suggestion).limit(per_page)
6 7 if is_cache_expired?(profile.manage_friends_cache_key(params))
7 8 @friends = profile.friends.paginate(:per_page => per_page, :page => params[:npage])
8 9 end
... ... @@ -16,6 +17,30 @@ class FriendsController &lt; MyProfileController
16 17 end
17 18 end
18 19  
  20 + def suggest
  21 + @suggestions = profile.profile_suggestions.of_person.enabled.includes(:suggestion).limit(per_page)
  22 + end
  23 +
  24 + def remove_suggestion
  25 + @person = profile.suggested_people.find_by_identifier(params[:id])
  26 + redirect_to :action => 'suggest' unless @person
  27 + if @person && request.post?
  28 + profile.remove_suggestion(@person)
  29 + @suggestions = profile.profile_suggestions.of_person.enabled.includes(:suggestion).limit(per_page)
  30 + render :partial => 'shared/profile_suggestions_list', :locals => { :suggestions => @suggestions, :collection => :friends_suggestions, :per_page => params[:per_page] || per_page }
  31 + end
  32 + end
  33 +
  34 + def connections
  35 + @suggestion = profile.profile_suggestions.of_person.enabled.find_by_suggestion_id(params[:id])
  36 + if @suggestion
  37 + @tags = @suggestion.tag_connections
  38 + @profiles = @suggestion.profile_connections
  39 + else
  40 + redirect_to :action => 'suggest'
  41 + end
  42 + end
  43 +
19 44 protected
20 45  
21 46 class << self
... ...
app/controllers/my_profile/memberships_controller.rb
... ... @@ -20,12 +20,54 @@ class MembershipsController &lt; MyProfileController
20 20 @community.environment = environment
21 21 @back_to = params[:back_to] || url_for(:action => 'index')
22 22 if request.post? && @community.valid?
23   - @community = Community.create_after_moderation(user, params[:community].merge({:environment => environment}))
24   - if @community.new_record?
  23 + begin
  24 + # Community was created
  25 + @community = Community.create_after_moderation(user, params[:community].merge({:environment => environment}))
  26 + @community.reload
  27 + redirect_to :action => 'welcome', :community_id => @community.id, :back_to => @back_to
  28 + rescue ActiveRecord::RecordNotFound
  29 + # Community pending approval
25 30 session[:notice] = _('Your new community creation request will be evaluated by an administrator. You will be notified.')
  31 + redirect_to @back_to
26 32 end
27   - redirect_to @back_to
28 33 return
29 34 end
30 35 end
  36 +
  37 + def welcome
  38 + @community = Community.find(params[:community_id])
  39 + @back_to = params[:back_to]
  40 + end
  41 +
  42 + def suggest
  43 + @suggestions = profile.profile_suggestions.of_community.enabled.includes(:suggestion).limit(per_page)
  44 + end
  45 +
  46 + def remove_suggestion
  47 + @community = profile.suggested_communities.find_by_identifier(params[:id])
  48 + custom_per_page = params[:per_page] || per_page
  49 + redirect_to :action => 'suggest' unless @community
  50 + if @community && request.post?
  51 + profile.remove_suggestion(@community)
  52 + @suggestions = profile.profile_suggestions.of_community.enabled.includes(:suggestion).limit(custom_per_page)
  53 + render :partial => 'shared/profile_suggestions_list', :locals => { :suggestions => @suggestions, :collection => :communities_suggestions, :per_page => custom_per_page}
  54 + end
  55 + end
  56 +
  57 + def connections
  58 + @suggestion = profile.profile_suggestions.of_community.enabled.find_by_suggestion_id(params[:id])
  59 + if @suggestion
  60 + @tags = @suggestion.tag_connections
  61 + @profiles = @suggestion.profile_connections
  62 + else
  63 + redirect_to :action => 'suggest'
  64 + end
  65 + end
  66 +
  67 + protected
  68 +
  69 + def per_page
  70 + 12
  71 + end
  72 +
31 73 end
... ...
app/controllers/my_profile/profile_editor_controller.rb
... ... @@ -3,6 +3,10 @@ class ProfileEditorController &lt; MyProfileController
3 3 protect 'edit_profile', :profile, :except => [:destroy_profile]
4 4 protect 'destroy_profile', :profile, :only => [:destroy_profile]
5 5  
  6 + before_filter :access_welcome_page, :only => [:welcome_page]
  7 + before_filter :back_to
  8 + helper_method :has_welcome_page
  9 +
6 10 def index
7 11 @pending_tasks = Task.to(profile).pending.without_spam.select{|i| user.has_permission?(i.permission, profile)}
8 12 end
... ... @@ -85,6 +89,21 @@ class ProfileEditorController &lt; MyProfileController
85 89 end
86 90 end
87 91  
  92 + def welcome_page
  93 + @welcome_page = profile.welcome_page || TinyMceArticle.new(:name => 'Welcome Page', :profile => profile, :published => false)
  94 + if request.post?
  95 + begin
  96 + @welcome_page.update_attributes!(params[:welcome_page])
  97 + profile.welcome_page = @welcome_page
  98 + profile.save!
  99 + session[:notice] = _('Welcome page saved successfully.')
  100 + redirect_to :action => 'index'
  101 + rescue Exception => exception
  102 + session[:notice] = _('Welcome page could not be saved.')
  103 + end
  104 + end
  105 + end
  106 +
88 107 def deactivate_profile
89 108 if environment.admins.include?(current_person)
90 109 profile = environment.profiles.find(params[:id])
... ... @@ -116,9 +135,24 @@ class ProfileEditorController &lt; MyProfileController
116 135 protected
117 136  
118 137 def redirect_to_previous_location
119   - back = request.referer
120   - back = "/" if back.nil?
  138 + redirect_to @back_to
  139 + end
121 140  
122   - redirect_to back
  141 + #TODO Consider using this as a general controller feature to be available on every action.
  142 + def back_to
  143 + @back_to = params[:back_to] || request.referer || "/"
123 144 end
  145 +
  146 + private
  147 +
  148 + def has_welcome_page
  149 + profile.is_template
  150 + end
  151 +
  152 + def access_welcome_page
  153 + unless has_welcome_page
  154 + render_access_denied
  155 + end
  156 + end
  157 +
124 158 end
... ...
app/controllers/my_profile/profile_members_controller.rb
... ... @@ -20,7 +20,7 @@ class ProfileMembersController &lt; MyProfileController
20 20 redirect_to :action => :last_admin
21 21 elsif @person.define_roles(@roles, profile)
22 22 session[:notice] = _('Roles successfuly updated')
23   - redirect_to :controller => 'profile_editor'
  23 + redirect_to :action => 'index'
24 24 else
25 25 session[:notice] = _('Couldn\'t change the roles')
26 26 redirect_to :action => 'index'
... ...
app/controllers/public/account_controller.rb
... ... @@ -82,10 +82,12 @@ class AccountController &lt; ApplicationController
82 82 if @plugins.dispatch(:allow_user_registration).include?(false)
83 83 redirect_back_or_default(:controller => 'home')
84 84 session[:notice] = _("This environment doesn't allow user registration.")
  85 + return
85 86 end
86 87  
87 88 store_location(request.referer) unless params[:return_to] or session[:return_to]
88 89  
  90 + # Tranforming to boolean
89 91 @block_bot = !!session[:may_be_a_bot]
90 92 @invitation_code = params[:invitation_code]
91 93 begin
... ... @@ -129,8 +131,8 @@ class AccountController &lt; ApplicationController
129 131 check_join_in_community(@user)
130 132 go_to_signup_initial_page
131 133 else
  134 + redirect_to :controller => :home, :action => :welcome, :template_id => (@user.person.template && @user.person.template.id)
132 135 session[:notice] = _('Thanks for registering!')
133   - @register_pending = true
134 136 end
135 137 end
136 138 end
... ... @@ -461,6 +463,8 @@ class AccountController &lt; ApplicationController
461 463 redirect_to user.url
462 464 when 'user_control_panel'
463 465 redirect_to user.admin_url
  466 + when 'welcome_page'
  467 + redirect_to :controller => :home, :action => :welcome, :template_id => (user.template && user.template.id)
464 468 else
465 469 redirect_back_or_default(default)
466 470 end
... ...
app/controllers/public/chat_controller.rb
... ... @@ -6,6 +6,7 @@ class ChatController &lt; PublicController
6 6 def start_session
7 7 login = user.jid
8 8 password = current_user.crypted_password
  9 + session[:chat] ||= {:rooms => []}
9 10 begin
10 11 jid, sid, rid = RubyBOSH.initialize_session(login, password, "http://#{environment.default_hostname}/http-bind",
11 12 :wait => 30, :hold => 1, :window => 5)
... ... @@ -16,6 +17,31 @@ class ChatController &lt; PublicController
16 17 end
17 18 end
18 19  
  20 + def toggle
  21 + session[:chat][:status] = session[:chat][:status] == 'opened' ? 'closed' : 'opened'
  22 + render :nothing => true
  23 + end
  24 +
  25 + def tab
  26 + session[:chat][:tab_id] = params[:tab_id]
  27 + render :nothing => true
  28 + end
  29 +
  30 + def join
  31 + session[:chat][:rooms] << params[:room_id]
  32 + session[:chat][:rooms].uniq!
  33 + render :nothing => true
  34 + end
  35 +
  36 + def leave
  37 + session[:chat][:rooms].delete(params[:room_id])
  38 + render :nothing => true
  39 + end
  40 +
  41 + def my_session
  42 + render :text => session[:chat].to_json, :layout => false
  43 + end
  44 +
19 45 def avatar
20 46 profile = environment.profiles.find_by_identifier(params[:id])
21 47 filename, mimetype = profile_icon(profile, :minor, true)
... ... @@ -28,15 +54,6 @@ class ChatController &lt; PublicController
28 54 end
29 55 end
30 56  
31   - def index
32   - presence = current_user.last_chat_status
33   - if presence.blank? or presence == 'chat'
34   - render :action => 'auto_connect_online'
35   - else
36   - render :action => 'auto_connect_busy'
37   - end
38   - end
39   -
40 57 def update_presence_status
41 58 if request.xhr?
42 59 current_user.update_attributes({:chat_status_at => DateTime.now}.merge(params[:status] || {}))
... ... @@ -44,6 +61,44 @@ class ChatController &lt; PublicController
44 61 render :nothing => true
45 62 end
46 63  
  64 + def save_message
  65 + to = environment.profiles.find_by_identifier(params[:to])
  66 + body = params[:body]
  67 +
  68 + ChatMessage.create!(:to => to, :from => user, :body => body)
  69 + render :text => 'ok'
  70 + end
  71 +
  72 + def recent_messages
  73 + other = environment.profiles.find_by_identifier(params[:identifier])
  74 + if other.kind_of?(Organization)
  75 + messages = ChatMessage.where('to_id=:other', :other => other.id)
  76 + else
  77 + messages = ChatMessage.where('(to_id=:other and from_id=:me) or (to_id=:me and from_id=:other)', {:me => user.id, :other => other.id})
  78 + end
  79 +
  80 + messages = messages.order('created_at DESC').includes(:to, :from).offset(params[:offset]).limit(20)
  81 + messages_json = messages.map do |message|
  82 + {
  83 + :body => message.body,
  84 + :to => {:id => message.to.identifier, :name => message.to.name},
  85 + :from => {:id => message.from.identifier, :name => message.from.name},
  86 + :created_at => message.created_at
  87 + }
  88 + end
  89 + render :json => messages_json.reverse
  90 + end
  91 +
  92 + def recent_conversations
  93 + conversations_order = ActiveRecord::Base.connection.execute("select profiles.identifier from profiles inner join (select distinct r.id as id, MAX(r.created_at) as created_at from (select from_id, to_id, created_at, (case when from_id=#{user.id} then to_id else from_id end) as id from chat_messages where from_id=#{user.id} or to_id=#{user.id}) as r group by id order by created_at desc, id) as t on profiles.id=t.id order by t.created_at desc").entries.map {|e| e['identifier']}
  94 + render :json => {:order => conversations_order.reverse, :domain => environment.default_hostname.gsub('.','-')}.to_json
  95 + end
  96 +
  97 + #TODO Ideally this is done through roster table on ejabberd.
  98 + def roster_groups
  99 + render :text => user.memberships.map {|m| {:jid => m.jid, :name => m.name}}.to_json
  100 + end
  101 +
47 102 protected
48 103  
49 104 def check_environment_feature
... ...
app/controllers/public/contact_controller.rb
1 1 class ContactController < PublicController
2 2  
3   - before_filter :login_required
4   -
5 3 needs_profile
  4 + before_filter :allow_access_to_page
6 5  
7 6 def new
8   - @contact
  7 + @contact = build_contact
9 8 if request.post? && params[:confirm] == 'true'
10   - @contact = user.build_contact(profile, params[:contact])
11 9 @contact.city = (!params[:city].blank? && City.exists?(params[:city])) ? City.find(params[:city]).name : nil
12 10 @contact.state = (!params[:state].blank? && State.exists?(params[:state])) ? State.find(params[:state]).name : nil
13 11 if @contact.deliver
... ... @@ -16,8 +14,17 @@ class ContactController &lt; PublicController
16 14 else
17 15 session[:notice] = _('Contact not sent')
18 16 end
  17 + end
  18 + end
  19 +
  20 + protected
  21 +
  22 + def build_contact
  23 + params[:contact] ||= {}
  24 + if logged_in?
  25 + user.build_contact profile, params[:contact]
19 26 else
20   - @contact = user.build_contact(profile)
  27 + Contact.new params[:contact].merge(dest: profile)
21 28 end
22 29 end
23 30  
... ...
app/controllers/public/events_controller.rb
1 1 class EventsController < PublicController
2 2  
3 3 needs_profile
  4 + before_filter :allow_access_to_page
4 5  
5 6 def events
6 7 @events = []
... ...
app/controllers/public/home_controller.rb
... ... @@ -2,13 +2,13 @@ class HomeController &lt; PublicController
2 2  
3 3 def index
4 4 @has_news = false
5   - if environment.enabled?('use_portal_community') && environment.portal_community
  5 + if environment.portal_enabled
6 6 @has_news = true
7 7 @news_cache_key = environment.portal_news_cache_key(FastGettext.locale)
8 8 if !read_fragment(@news_cache_key)
9 9 portal_community = environment.portal_community
10   - @highlighted_news = portal_community.news(2, true)
11   - @portal_news = portal_community.news(7, true) - @highlighted_news
  10 + @highlighted_news = portal_community.news(environment.highlighted_news_amount, true)
  11 + @portal_news = portal_community.news(environment.portal_news_amount, true).offset(environment.highlighted_news_amount)
12 12 @area_news = environment.portal_folders
13 13 end
14 14 end
... ... @@ -18,4 +18,10 @@ class HomeController &lt; PublicController
18 18 @no_design_blocks = true
19 19 end
20 20  
  21 + def welcome
  22 + @no_design_blocks = true
  23 + @display_confirmation_tips = !user.present? && !environment.enabled?(:skip_new_user_email_confirmation)
  24 + @person_template = user && user.template || params[:template_id] && Person.find(params[:template_id])
  25 + end
  26 +
21 27 end
... ...
app/controllers/public/invite_controller.rb
... ... @@ -4,8 +4,15 @@ class InviteController &lt; PublicController
4 4 before_filter :login_required
5 5 before_filter :check_permissions_to_invite
6 6  
7   - def select_address_book
  7 + def invite_friends
8 8 @import_from = params[:import_from] || "manual"
  9 + @mail_template = params[:mail_template] || environment.invitation_mail_template(profile)
  10 +
  11 + labels = Profile::SEARCHABLE_FIELDS.except(:nickname).merge(User::SEARCHABLE_FIELDS).map { |name,info| info[:label].downcase }
  12 + last = labels.pop
  13 + label = labels.join(', ')
  14 + @search_fields = "#{label} #{_('or')} #{last}"
  15 +
9 16 if request.post?
10 17 contact_list = ContactList.create
11 18 Delayed::Job.enqueue GetEmailContactsJob.new(@import_from, params[:login], params[:password], contact_list.id) if @import_from != 'manual'
... ... @@ -22,7 +29,7 @@ class InviteController &lt; PublicController
22 29 webmail_import_addresses = params[:webmail_import_addresses]
23 30 contacts_to_invite = Invitation.join_contacts(manual_import_addresses, webmail_import_addresses)
24 31 if !contacts_to_invite.empty?
25   - Delayed::Job.enqueue InvitationJob.new(current_user.person.id, contacts_to_invite, params[:mail_template], profile.id, @contact_list.id, locale)
  32 + Delayed::Job.enqueue InvitationJob.new(user.id, contacts_to_invite, params[:mail_template], profile.id, @contact_list.id, locale)
26 33 session[:notice] = _('Your invitations are being sent.')
27 34 if profile.person?
28 35 redirect_to :controller => 'profile', :action => 'friends'
... ... @@ -52,16 +59,36 @@ class InviteController &lt; PublicController
52 59 def cancel_fetching_emails
53 60 contact_list = ContactList.find(params[:contact_list])
54 61 contact_list.destroy
55   - redirect_to :action => 'select_address_book'
  62 + redirect_to :action => 'invite_friends'
  63 + end
  64 +
  65 + def invite_registered_friend
  66 + contacts_to_invite = params['q'].split(',')
  67 + if !contacts_to_invite.empty? && request.post?
  68 + Delayed::Job.enqueue InvitationJob.new(user.id, contacts_to_invite, '', profile.id, nil, locale)
  69 + session[:notice] = _('Your invitations are being sent.')
  70 + if profile.person?
  71 + redirect_to :controller => 'profile', :action => 'friends'
  72 + else
  73 + redirect_to :controller => 'profile', :action => 'members'
  74 + end
  75 + else
  76 + redirect_to :action => 'invite_friends'
  77 + session[:notice] = _('Please enter a valid profile.')
  78 + end
  79 + end
  80 +
  81 + def search
  82 + scope = profile.invite_friends_only ? user.friends : environment.people
  83 + scope = scope.not_members_of(profile) if profile.organization?
  84 + scope = scope.not_friends_of(profile) if profile.person?
  85 + results = find_by_contents(:people, environment, scope, params['q'], {:page => 1}, {:joins => :user})[:results]
  86 + render :text => prepare_to_token_input(results).to_json
56 87 end
57 88  
58 89 protected
59 90  
60 91 def check_permissions_to_invite
61   - if profile.person? and !current_user.person.has_permission?(:manage_friends, profile) or
62   - profile.community? and !current_user.person.has_permission?(:invite_members, profile)
63   - render_access_denied
64   - end
  92 + render_access_denied if !profile.allow_invitation_from?(user)
65 93 end
66   -
67 94 end
... ...
app/controllers/public/profile_controller.rb
... ... @@ -16,13 +16,7 @@ class ProfileController &lt; PublicController
16 16 @activities = @profile.activities.paginate(:per_page => 15, :page => params[:page])
17 17 end
18 18 @tags = profile.article_tags
19   - unless profile.display_info_to?(user)
20   - if profile.visible?
21   - private_profile
22   - else
23   - invisible_profile
24   - end
25   - end
  19 + allow_access_to_page
26 20 end
27 21  
28 22 def tags
... ... @@ -396,17 +390,6 @@ class ProfileController &lt; PublicController
396 390 end
397 391 end
398 392  
399   - def private_profile
400   - private_profile_partial_parameters
401   - render :action => 'index', :status => 403
402   - end
403   -
404   - def invisible_profile
405   - unless profile.is_template?
406   - render_access_denied(_("This profile is inaccessible. You don't have the permission to view the content here."), _("Oops ... you cannot go ahead here"))
407   - end
408   - end
409   -
410 393 def per_page
411 394 Noosfero::Constants::PROFILE_PER_PAGE
412 395 end
... ...
app/controllers/public/profile_search_controller.rb
... ... @@ -9,9 +9,12 @@ class ProfileSearchController &lt; PublicController
9 9 @q = params[:q]
10 10 unless @q.blank?
11 11 if params[:where] == 'environment'
12   - redirect_to :controller => 'search', :query => @q
  12 + # user is using global search, redirects to the search controller with
  13 + # the query
  14 + search_path = url_for(:controller => 'search', :query => @q)
  15 + request.xhr? ? render(:js => "window.location.href = #{search_path.to_json}") : redirect_to(search_path)
13 16 else
14   - @results = find_by_contents(:articles, profile.articles.published, @q, {:per_page => 10, :page => params[:page]})[:results]
  17 + @results = find_by_contents(:articles, profile, profile.articles.published, @q, {:per_page => 10, :page => params[:page]})[:results]
15 18 end
16 19 end
17 20 end
... ...
app/controllers/public/search_controller.rb
... ... @@ -4,11 +4,11 @@ class SearchController &lt; PublicController
4 4 include SearchHelper
5 5 include ActionView::Helpers::NumberHelper
6 6  
7   - before_filter :redirect_asset_param, :except => :assets
8   - before_filter :load_category
9   - before_filter :load_search_assets
10   - before_filter :load_query
11   - before_filter :load_filter
  7 + before_filter :redirect_asset_param, :except => [:assets, :suggestions]
  8 + before_filter :load_category, :except => :suggestions
  9 + before_filter :load_search_assets, :except => :suggestions
  10 + before_filter :load_query, :except => :suggestions
  11 + before_filter :load_order, :except => :suggestions
12 12  
13 13 # Backwards compatibility with old URLs
14 14 def redirect_asset_param
... ... @@ -20,7 +20,7 @@ class SearchController &lt; PublicController
20 20  
21 21 def index
22 22 @searches = {}
23   - @order = []
  23 + @assets = []
24 24 @names = {}
25 25 @results_only = true
26 26  
... ... @@ -28,7 +28,7 @@ class SearchController &lt; PublicController
28 28 load_query
29 29 @asset = key
30 30 send(key)
31   - @order << key
  31 + @assets << key
32 32 @names[key] = _(description)
33 33 end
34 34 @asset = nil
... ... @@ -42,7 +42,7 @@ class SearchController &lt; PublicController
42 42 # view the summary of one category
43 43 def category_index
44 44 @searches = {}
45   - @order = []
  45 + @assets = []
46 46 @names = {}
47 47 limit = MULTIPLE_SEARCH_LIMIT
48 48 [
... ... @@ -53,7 +53,7 @@ class SearchController &lt; PublicController
53 53 [ :communities, _('Communities'), :recent_communities ],
54 54 [ :articles, _('Contents'), :recent_articles ]
55 55 ].each do |asset, name, filter|
56   - @order << asset
  56 + @assets << asset
57 57 @searches[asset]= {:results => @category.send(filter, limit)}
58 58 raise "No total_entries for: #{asset}" unless @searches[asset][:results].respond_to?(:total_entries)
59 59 @names[asset] = name
... ... @@ -147,12 +147,16 @@ class SearchController &lt; PublicController
147 147 render :partial => 'events/events'
148 148 end
149 149  
  150 + def suggestions
  151 + render :text => find_suggestions(normalize_term(params[:term]), environment, params[:asset]).to_json
  152 + end
  153 +
150 154 #######################################################
151 155 protected
152 156  
153 157 def load_query
154 158 @asset = (params[:asset] || params[:action]).to_sym
155   - @order ||= [@asset]
  159 + @assets ||= [@asset]
156 160 @searches ||= {}
157 161  
158 162 @query = params[:query] || ''
... ... @@ -173,13 +177,22 @@ class SearchController &lt; PublicController
173 177 end
174 178 end
175 179  
  180 + AVAILABLE_SEARCHES = ActiveSupport::OrderedHash[
  181 + :articles, _('Contents'),
  182 + :people, _('People'),
  183 + :communities, _('Communities'),
  184 + :enterprises, _('Enterprises'),
  185 + :products, _('Products and Services'),
  186 + :events, _('Events'),
  187 + ]
  188 +
176 189 def load_search_assets
177   - if SEARCHES.keys.include?(params[:action].to_sym) && environment.enabled?("disable_asset_#{params[:action]}")
  190 + if AVAILABLE_SEARCHES.keys.include?(params[:action].to_sym) && environment.enabled?("disable_asset_#{params[:action]}")
178 191 render_not_found
179 192 return
180 193 end
181 194  
182   - @enabled_searches = SEARCHES.select {|key, name| environment.disabled?("disable_asset_#{key}") }
  195 + @enabled_searches = AVAILABLE_SEARCHES.select {|key, name| environment.disabled?("disable_asset_#{key}") }
183 196 @searching = {}
184 197 @titles = {}
185 198 @enabled_searches.each do |key, name|
... ... @@ -189,11 +202,11 @@ class SearchController &lt; PublicController
189 202 @names = @titles if @names.nil?
190 203 end
191 204  
192   - def load_filter
193   - @filter = 'more_recent'
194   - if SEARCHES.keys.include?(@asset.to_sym)
195   - available_filters = asset_class(@asset)::SEARCH_FILTERS
196   - @filter = params[:filter] if available_filters.include?(params[:filter])
  205 + def load_order
  206 + @order = 'more_recent'
  207 + if AVAILABLE_SEARCHES.keys.include?(@asset.to_sym)
  208 + available_orders = asset_class(@asset)::SEARCH_FILTERS[:order]
  209 + @order = params[:order] if available_orders.include?(params[:order])
197 210 end
198 211 end
199 212  
... ... @@ -217,7 +230,7 @@ class SearchController &lt; PublicController
217 230 end
218 231  
219 232 def full_text_search
220   - @searches[@asset] = find_by_contents(@asset, @scope, @query, paginate_options, {:category => @category, :filter => @filter})
  233 + @searches[@asset] = find_by_contents(@asset, environment, @scope, @query, paginate_options, {:category => @category, :filter => @order})
221 234 end
222 235  
223 236 private
... ... @@ -232,4 +245,14 @@ class SearchController &lt; PublicController
232 245 20
233 246 end
234 247  
  248 + def available_assets
  249 + assets = ActiveSupport::OrderedHash[
  250 + :articles, _('Contents'),
  251 + :enterprises, _('Enterprises'),
  252 + :people, _('People'),
  253 + :communities, _('Communities'),
  254 + :products, _('Products and Services'),
  255 + ]
  256 + end
  257 +
235 258 end
... ...
app/controllers/public_controller.rb
1 1 class PublicController < ApplicationController
  2 + protected
  3 +
  4 + def allow_access_to_page
  5 + unless profile.display_info_to?(user)
  6 + if profile.visible?
  7 + private_profile
  8 + else
  9 + invisible_profile
  10 + end
  11 + end
  12 + end
  13 +
  14 + def private_profile
  15 + private_profile_partial_parameters
  16 + render :template => 'profile/_private_profile', :status => 403, :formats => [:html]
  17 + end
  18 +
  19 + def invisible_profile
  20 + unless profile.is_template?
  21 + render_access_denied(_("This profile is inaccessible. You don't have the permission to view the content here."), _("Oops ... you cannot go ahead here"))
  22 + end
  23 + end
2 24 end
... ...
app/helpers/application_helper.rb
... ... @@ -8,11 +8,7 @@ module ApplicationHelper
8 8  
9 9 include PermissionNameHelper
10 10  
11   - include LightboxHelper
12   -
13   - include ThickboxHelper
14   -
15   - include ColorboxHelper
  11 + include ModalHelper
16 12  
17 13 include BoxesHelper
18 14  
... ... @@ -46,6 +42,8 @@ module ApplicationHelper
46 42  
47 43 include CatalogHelper
48 44  
  45 + include PluginsHelper
  46 +
49 47 def locale
50 48 (@page && !@page.language.blank?) ? @page.language : FastGettext.locale
51 49 end
... ... @@ -594,7 +592,7 @@ module ApplicationHelper
594 592 extra_info = extra_info.nil? ? '' : content_tag( 'span', extra_info, :class => 'extra_info' )
595 593 links = links_for_balloon(profile)
596 594 content_tag('div', content_tag(tag,
597   - (environment.enabled?(:show_balloon_with_profile_links_when_clicked) ? link_to( content_tag( 'span', _('Profile links')), '#', :onclick => "toggleSubmenu(this, '#{profile.short_name}', #{CGI::escapeHTML(links.to_json)}); return false", :class => "menu-submenu-trigger #{trigger_class}", :url => url) : "") +
  595 + (environment.enabled?(:show_balloon_with_profile_links_when_clicked) ? popover_menu(_('Profile links'),profile.short_name,links,{:class => trigger_class, :url => url}) : "") +
598 596 link_to(
599 597 content_tag( 'span', profile_image( profile, size ), :class => 'profile-image' ) +
600 598 content_tag( 'span', h(name), :class => ( profile.class == Person ? 'fn' : 'org' ) ) +
... ... @@ -606,6 +604,14 @@ module ApplicationHelper
606 604 :class => 'vcard'), :class => 'common-profile-list-block')
607 605 end
608 606  
  607 + def popover_menu(title,menu_title,links,html_options={})
  608 + html_options[:class] = "" unless html_options[:class]
  609 + html_options[:class] << " menu-submenu-trigger"
  610 + html_options[:onclick] = "toggleSubmenu(this, '#{menu_title}', #{CGI::escapeHTML(links.to_json)}); return false"
  611 +
  612 + link_to(content_tag(:span, title), '#', html_options)
  613 + end
  614 +
609 615 def gravatar_default
610 616 (respond_to?(:theme_option) && theme_option.present? && theme_option['gravatar']) || NOOSFERO_CONF['gravatar'] || 'mm'
611 617 end
... ... @@ -649,8 +655,8 @@ module ApplicationHelper
649 655 ' onfocus="if(this.value==\''+s+'\'){this.value=\'\'} this.form.className=\'focus-in\'"'+
650 656 ' onblur="if(/^\s*$/.test(this.value)){this.value=\''+s+'\'} this.form.className=\'focus-out\'">'+
651 657 '</form>'
652   - else #opt == 'lightbox_link' is default
653   - lightbox_link_to '<span class="icon-menu-search"></span>'+ _('Search'), {
  658 + else
  659 + modal_link_to '<span class="icon-menu-search"></span>'+ _('Search'), {
654 660 :controller => 'search',
655 661 :action => 'popup',
656 662 :category_path => (@category ? @category.explode_path : nil)},
... ... @@ -720,7 +726,7 @@ module ApplicationHelper
720 726 class NoosferoFormBuilder < ActionView::Helpers::FormBuilder
721 727 extend ActionView::Helpers::TagHelper
722 728  
723   - def self.output_field(text, field_html, field_id = nil)
  729 + def self.output_field(text, field_html, field_id = nil, options = {})
724 730 # try to guess an id if none given
725 731 if field_id.nil?
726 732 field_html =~ /id=['"]([^'"]*)['"]/
... ... @@ -862,8 +868,9 @@ module ApplicationHelper
862 868 end
863 869  
864 870 def base_url
865   - environment.top_url
  871 + environment.top_url(request.scheme)
866 872 end
  873 + alias :top_url :base_url
867 874  
868 875 def helper_for_article(article)
869 876 article_helper = ActionView::Base.new
... ... @@ -1047,11 +1054,11 @@ module ApplicationHelper
1047 1054 {s_('contents|Most commented') => {:href => url_for({:controller => 'search', :action => 'contents', :filter => 'more_comments'})}}
1048 1055 ]
1049 1056 if logged_in?
1050   - links.push(_('New content') => colorbox_options({:href => url_for({:controller => 'cms', :action => 'new', :profile => current_user.login, :cms => true})}))
  1057 + links.push(_('New content') => modal_options({:href => url_for({:controller => 'cms', :action => 'new', :profile => current_user.login, :cms => true})}))
1051 1058 end
1052 1059  
1053 1060 link_to(content_tag(:span, _('Contents'), :class => 'icon-menu-articles'), {:controller => "search", :action => 'contents', :category_path => nil}, :id => 'submenu-contents') +
1054   - link_to(content_tag(:span, _('Contents menu')), '#', :onclick => "toggleSubmenu(this,'',#{CGI::escapeHTML(links.to_json)}); return false", :class => 'menu-submenu-trigger up', :id => 'submenu-contents-trigger')
  1061 + popover_menu(_('Contents menu'),'',links,:class => 'up', :id => 'submenu-contents-trigger')
1055 1062 end
1056 1063 alias :browse_contents_menu :search_contents_menu
1057 1064  
... ... @@ -1067,7 +1074,7 @@ module ApplicationHelper
1067 1074 end
1068 1075  
1069 1076 link_to(content_tag(:span, _('People'), :class => 'icon-menu-people'), {:controller => "search", :action => 'people', :category_path => ''}, :id => 'submenu-people') +
1070   - link_to(content_tag(:span, _('People menu')), '#', :onclick => "toggleSubmenu(this,'',#{CGI::escapeHTML(links.to_json)}); return false", :class => 'menu-submenu-trigger up', :id => 'submenu-people-trigger')
  1077 + popover_menu(_('People menu'),'',links,:class => 'up', :id => 'submenu-people-trigger')
1071 1078 end
1072 1079 alias :browse_people_menu :search_people_menu
1073 1080  
... ... @@ -1083,7 +1090,7 @@ module ApplicationHelper
1083 1090 end
1084 1091  
1085 1092 link_to(content_tag(:span, _('Communities'), :class => 'icon-menu-community'), {:controller => "search", :action => 'communities'}, :id => 'submenu-communities') +
1086   - link_to(content_tag(:span, _('Communities menu')), '#', :onclick => "toggleSubmenu(this,'',#{CGI::escapeHTML(links.to_json)}); return false", :class => 'menu-submenu-trigger up', :id => 'submenu-communities-trigger')
  1093 + popover_menu(_('Communities menu'),'',links,:class => 'up', :id => 'submenu-communities-trigger')
1087 1094 end
1088 1095 alias :browse_communities_menu :search_communities_menu
1089 1096  
... ... @@ -1306,8 +1313,19 @@ module ApplicationHelper
1306 1313 end
1307 1314 end
1308 1315  
1309   - def remove_content_button(action)
1310   - @plugins.dispatch("content_remove_#{action.to_s}", @page).include?(true)
  1316 + def content_remove_spread(content)
  1317 + !content.public? || content.folder? || (profile == user && user.communities.blank? && !environment.portal_enabled)
  1318 + end
  1319 +
  1320 + def remove_content_button(action, content)
  1321 + method_name = "content_remove_#{action.to_s}"
  1322 + plugin_condition = @plugins.dispatch(method_name, content).include?(true)
  1323 + begin
  1324 + core_condition = self.send(method_name, content)
  1325 + rescue NoMethodError
  1326 + core_condition = false
  1327 + end
  1328 + core_condition || plugin_condition
1311 1329 end
1312 1330  
1313 1331 def template_options(kind, field_name)
... ... @@ -1384,16 +1402,16 @@ module ApplicationHelper
1384 1402 end
1385 1403  
1386 1404 def convert_macro(html, source)
1387   - doc = Hpricot(html)
  1405 + doc = Nokogiri::HTML.fragment html
1388 1406 #TODO This way is more efficient but do not support macro inside of
1389 1407 # macro. You must parse them from the inside-out in order to enable
1390 1408 # that.
1391   - doc.search('.macro').each do |macro|
  1409 + doc.css('.macro').each do |macro|
1392 1410 macro_name = macro['data-macro']
1393 1411 result = @plugins.parse_macro(macro_name, macro, source)
1394 1412 macro.inner_html = result.kind_of?(Proc) ? self.instance_exec(&result) : result
1395 1413 end
1396   - doc.html
  1414 + doc.to_html
1397 1415 end
1398 1416  
1399 1417 def default_folder_for_image_upload(profile)
... ... @@ -1410,6 +1428,43 @@ module ApplicationHelper
1410 1428 content_tag('ul', article.versions.map {|v| link_to("r#{v.version}", @page.url.merge(:version => v.version))})
1411 1429 end
1412 1430  
  1431 + def search_input_with_suggestions(name, asset, default, options = {})
  1432 + text_field_tag name, default, options.merge({:class => 'search-input-with-suggestions', 'data-asset' => asset})
  1433 + end
  1434 +
  1435 + def profile_suggestion_profile_connections(suggestion)
  1436 + profiles = suggestion.profile_connections.first(4).map do |profile|
  1437 + link_to(profile_image(profile, :icon, :title => profile.name), profile.url, :class => 'profile-suggestion-connection-icon')
  1438 + end
  1439 +
  1440 + controller_target = suggestion.suggestion_type == 'Person' ? :friends : :memberships
  1441 + profiles << link_to("<big> +#{suggestion.profile_connections.count - 4}</big>", :controller => controller_target, :action => :connections, :id => suggestion.suggestion_id) if suggestion.profile_connections.count > 4
  1442 +
  1443 + if profiles.present?
  1444 + content_tag(:div, profiles.join , :class => 'profile-connections')
  1445 + else
  1446 + ''
  1447 + end
  1448 + end
  1449 +
  1450 + def profile_suggestion_tag_connections(suggestion)
  1451 + tags = suggestion.tag_connections.first(4).map do |tag|
  1452 + tag.name + ', '
  1453 + end
  1454 + last_tag = tags.pop
  1455 + tags << last_tag.strip.chop if last_tag.present?
  1456 + title = tags.join
  1457 +
  1458 + controller_target = suggestion.suggestion_type == 'Person' ? :friends : :memberships
  1459 + tags << ' ' + link_to('...', {:controller => controller_target, :action => :connections, :id => suggestion.suggestion_id}, :class => 'more-tag-connections', :title => _('See all connections')) if suggestion.tag_connections.count > 4
  1460 +
  1461 + if tags.present?
  1462 + content_tag(:div, tags.join, :class => 'tag-connections', :title => title)
  1463 + else
  1464 + ''
  1465 + end
  1466 + end
  1467 +
1413 1468 def labelled_colorpicker_field(human_name, object_name, method, options = {})
1414 1469 options[:id] ||= 'text-field-' + FormsHelper.next_id_number
1415 1470 content_tag('label', human_name, :for => options[:id], :class => 'formlabel') +
... ...
app/helpers/article_helper.rb
1 1 module ArticleHelper
2 2  
3   - include PrototypeHelper
4 3 include TokenHelper
5 4  
6 5 def article_reported_version(article)
... ... @@ -35,7 +34,7 @@ module ArticleHelper
35 34 'div',
36 35 check_box(:article, :notify_comments) +
37 36 content_tag('label', _('I want to receive a notification of each comment written by e-mail'), :for => 'article_notify_comments') +
38   - observe_field(:article_accept_comments, :function => "$('article_notify_comments').disabled = ! $('article_accept_comments').checked;$('article_moderate_comments').disabled = ! $('article_accept_comments').checked")
  37 + observe_field(:article_accept_comments, :function => "jQuery('#article_notify_comments')[0].disabled = ! jQuery('#article_accept_comments')[0].checked;jQuery('#article_moderate_comments')[0].disabled = ! jQuery('#article_accept_comments')[0].checked")
39 38 ) +
40 39  
41 40 content_tag(
... ...
app/helpers/block_helper.rb
... ... @@ -19,7 +19,7 @@ module BlockHelper
19 19 content_tag('span', _('Title')) +
20 20 text_field_tag('block[images][][title]', image[:title], :class => 'highlight-title', :size => 45)
21 21 }</label></td>
22   - <td>#{link_to '', '#', :class=>'button icon-button icon-delete delete-highlight', :confirm=>_('Are you sure you want to remove this highlight')}</td>
  22 + <td>#{button_without_text(:delete, _('Remove'), '#', class: 'delete-highlight', :confirm=>_('Are you sure you want to remove this highlight'))}</td>
23 23 </tr>
24 24 "
25 25 end
... ...
app/helpers/boxes_helper.rb
... ... @@ -38,8 +38,12 @@ module BoxesHelper
38 38 end
39 39 end
40 40  
  41 + def boxes_limit holder
  42 + controller.send(:custom_design)[:boxes_limit] || holder.boxes_limit(controller.send(:custom_design)[:layout_template])
  43 + end
  44 +
41 45 def display_boxes(holder, main_content)
42   - boxes = holder.boxes.with_position.first(holder.boxes_limit)
  46 + boxes = holder.boxes.with_position.first(boxes_limit(holder))
43 47 content = boxes.reverse.map { |item| display_box(item, main_content) }.join("\n")
44 48 content = main_content if (content.blank?)
45 49  
... ... @@ -65,11 +69,13 @@ module BoxesHelper
65 69 end
66 70  
67 71 def display_box_content(box, main_content)
68   - context = { :article => @page, :request_path => request.path, :locale => locale, :params => request.params, :user => user }
69   - box_decorator.select_blocks(box.blocks.includes(:box), context).map { |item| display_block(item, main_content) }.join("\n") + box_decorator.block_target(box)
  72 + context = { :article => @page, :request_path => request.path, :locale => locale, :params => request.params, :user => user, :controller => controller }
  73 + box_decorator.select_blocks(box, box.blocks.includes(:box), context).map do |item|
  74 + display_block item, main_content
  75 + end.join("\n") + box_decorator.block_target(box)
70 76 end
71 77  
72   - def select_blocks(arr, context)
  78 + def select_blocks box, arr, context
73 79 arr
74 80 end
75 81  
... ... @@ -150,8 +156,22 @@ module BoxesHelper
150 156 def self.block_edit_buttons(block)
151 157 ''
152 158 end
153   - def self.select_blocks(arr, context)
154   - arr.select { |block| block.visible?(context) }
  159 + def self.select_blocks box, arr, context
  160 + arr = arr.select{ |block| block.visible? context }
  161 +
  162 + custom_design = context[:controller].send(:custom_design)
  163 + inserts = [custom_design[:insert]].flatten.compact
  164 + inserts.each do |insert_opts|
  165 + next unless box.position == insert_opts[:box]
  166 + position, block = insert_opts[:position], insert_opts[:block]
  167 + block = block.new box: box if block.is_a? Class
  168 +
  169 + if not insert_opts[:uniq] or not box.blocks.map(&:class).include? block.klass
  170 + arr = arr.insert position, block
  171 + end
  172 + end
  173 +
  174 + arr
155 175 end
156 176 end
157 177  
... ... @@ -211,7 +231,7 @@ module BoxesHelper
211 231 end
212 232  
213 233 if block.editable?
214   - buttons << colorbox_icon_button(:edit, _('Edit'), { :action => 'edit', :id => block.id })
  234 + buttons << modal_icon_button(:edit, _('Edit'), { :action => 'edit', :id => block.id })
215 235 end
216 236  
217 237 if !block.main?
... ... @@ -221,7 +241,7 @@ module BoxesHelper
221 241 end
222 242  
223 243 if block.respond_to?(:help)
224   - buttons << thickbox_inline_popup_icon(:help, _('Help on this block'), {}, "help-on-box-#{block.id}") << content_tag('div', content_tag('h2', _('Help')) + content_tag('div', block.help, :style => 'margin-bottom: 1em;') + thickbox_close_button(_('Close')), :style => 'display: none;', :id => "help-on-box-#{block.id}")
  244 + buttons << modal_inline_icon(:help, _('Help on this block'), {}, "#help-on-box-#{block.id}") << content_tag('div', content_tag('h2', _('Help')) + content_tag('div', block.help, :style => 'margin-bottom: 1em;') + modal_close_button(_('Close')), :style => 'display: none;', :id => "help-on-box-#{block.id}")
225 245 end
226 246  
227 247 if block.embedable?
... ...
app/helpers/categories_helper.rb
... ... @@ -25,10 +25,13 @@ module CategoriesHelper
25 25 )
26 26 end
27 27  
28   - def update_categories_link(body, category_id=nil, html_options={})
  28 + #TODO: remove this function and, in views, use existing basic buttons
  29 + def update_categories_link(type, body, category_id=nil, html_options={})
  30 + html_class = 'select-subcategory-link'
  31 + html_class = " icon-#{type} btn btn-primary btn-xs" if type.present?
29 32 link_to body,
30 33 { :action => "update_categories", :category_id => category_id, :id => @object },
31   - {:id => category_id ? "select-category-#{category_id}-link" : nil, :remote => true, :class => 'select-subcategory-link'}.merge(html_options)
  34 + {:id => category_id ? "select-category-#{category_id}-link" : nil, :remote => true, :class => html_class}.merge(html_options)
32 35 end
33 36  
34 37 end
... ...
app/helpers/chat_helper.rb
... ... @@ -6,8 +6,9 @@ module ChatHelper
6 6 ['icon-menu-busy', _('Busy'), 'chat-busy'],
7 7 ['icon-menu-offline', _('Sign out of chat'), 'chat-disconnect'],
8 8 ]
  9 + avatar = profile_image(user, :portrait, :class => 'avatar')
9 10 content_tag('span',
10   - link_to(content_tag('span', status) + ui_icon('ui-icon-triangle-1-s'),
  11 + link_to(avatar + content_tag('span', user.name) + ui_icon('ui-icon-triangle-1-s'),
11 12 '#',
12 13 :onclick => 'toggleMenu(this); return false',
13 14 :class => icon_class + ' simplemenu-trigger'
... ...
app/helpers/cms_helper.rb
... ... @@ -40,12 +40,8 @@ module CmsHelper
40 40 end
41 41 end
42 42  
43   - def display_spread_button(profile, article)
44   - if profile.person?
45   - expirable_button article, :spread, _('Spread this'), :action => 'publish', :id => article.id
46   - elsif profile.community? && environment.portal_community
47   - expirable_button article, :spread, _('Spread this'), :action => 'publish_on_portal_community', :id => article.id
48   - end
  43 + def display_spread_button(article)
  44 + expirable_button article, :spread, _('Spread this'), {:action => 'publish', :id => article.id}, {:class => 'colorbox'}
49 45 end
50 46  
51 47 def display_delete_button(article)
... ...
app/helpers/colorbox_helper.rb
... ... @@ -1,25 +0,0 @@
1   -module ColorboxHelper
2   -
3   - def colorbox_close_button(text, options = {})
4   - button(:close, text, '#', colorbox_options(options, :close))
5   - end
6   -
7   - def colorbox_button(type, label, url, options = {})
8   - button(type, label, url, colorbox_options(options))
9   - end
10   -
11   - def colorbox_icon_button(type, label, url, options = {})
12   - icon_button(type, label, url, colorbox_options(options))
13   - end
14   -
15   - # options must be an HTML options hash as passed to link_to etc.
16   - #
17   - # returns a new hash with colorbox class added. Keeps existing classes.
18   - def colorbox_options(options, type=nil)
19   - the_class = 'colorbox'
20   - the_class += "-#{type.to_s}" unless type.nil?
21   - the_class << " #{options[:class]}" if options.has_key?(:class)
22   - options.merge(:class => the_class)
23   - end
24   -
25   -end
app/helpers/comment_helper.rb
... ... @@ -65,7 +65,7 @@ module CommentHelper
65 65  
66 66 def link_for_edit(comment)
67 67 if comment.can_be_updated_by?(user)
68   - {:link => expirable_comment_link(comment, :edit, _('Edit'), url_for(:profile => profile.identifier, :controller => :comment, :action => :edit, :id => comment.id),:class => 'colorbox')}
  68 + {:link => expirable_comment_link(comment, :edit, _('Edit'), url_for(:profile => profile.identifier, :controller => :comment, :action => :edit, :id => comment.id),:class => 'modal')}
69 69 end
70 70 end
71 71  
... ...
app/helpers/dates_helper.rb
... ... @@ -2,24 +2,14 @@ require &#39;noosfero/i18n&#39;
2 2  
3 3 module DatesHelper
4 4  
5   - # FIXME Date#strftime should translate this for us !!!!
6   - MONTHS = [
7   - N_('January'),
8   - N_('February'),
9   - N_('March'),
10   - N_('April'),
11   - N_('May'),
12   - N_('June'),
13   - N_('July'),
14   - N_('August'),
15   - N_('September'),
16   - N_('October'),
17   - N_('November'),
18   - N_('December')
19   - ]
20   -
21   - def month_name(n)
22   - _(MONTHS[n-1])
  5 + MONTHS = I18n.t('date.month_names')
  6 +
  7 + def month_name(n, abbreviated = false)
  8 + if abbreviated
  9 + I18n.t('date.abbr_month_names')[n]
  10 + else
  11 + MONTHS[n]
  12 + end
23 13 end
24 14  
25 15 # formats a date for displaying.
... ... @@ -91,15 +81,7 @@ module DatesHelper
91 81 _(date.strftime("%a"))
92 82 else
93 83 # FIXME Date#strftime should translate this for us !!!!
94   - _([
95   - N_('Sunday'),
96   - N_('Monday'),
97   - N_('Tuesday'),
98   - N_('Wednesday'),
99   - N_('Thursday'),
100   - N_('Friday'),
101   - N_('Saturday'),
102   - ][date.wday])
  84 + I18n.t('date.day_names')[date.wday]
103 85 end
104 86 end
105 87  
... ... @@ -111,7 +93,7 @@ module DatesHelper
111 93 date = date << 1
112 94 end
113 95 if opts[:only_month]
114   - _('%{month}') % {:month => month_name(date.month.to_i) }
  96 + _('%{month}') % { :month => month_name(date.month.to_i) }
115 97 else
116 98 _('%{month} %{year}') % { :year => date.year, :month => month_name(date.month.to_i) }
117 99 end
... ... @@ -156,7 +138,7 @@ module DatesHelper
156 138 else
157 139 order = [:day, :month, :year]
158 140 end
159   - date_select(object, method, html_options.merge(options.merge(:include_blank => true, :order => order, :use_month_names => MONTHS.map {|item| gettext(item)})))
  141 + date_select(object, method, html_options.merge(options.merge(:include_blank => true, :order => order, :use_month_names => MONTHS)))
160 142 end
161 143  
162 144 end
... ...
app/helpers/design_helper.rb 0 → 100644
... ... @@ -0,0 +1,50 @@
  1 +module DesignHelper
  2 +
  3 + extend ActiveSupport::Concern
  4 +
  5 + included do
  6 + extend ClassMethods
  7 + include InstanceMethods
  8 + before_filter :load_custom_design if self.respond_to? :before_filter
  9 + end
  10 +
  11 + module ClassMethods
  12 +
  13 + def no_design_blocks
  14 + @no_design_blocks = true
  15 + end
  16 +
  17 + def use_custom_design options = {}
  18 + @custom_design = options
  19 + end
  20 +
  21 + def custom_design
  22 + @custom_design ||= {}
  23 + end
  24 +
  25 + def uses_design_blocks?
  26 + !@no_design_blocks
  27 + end
  28 +
  29 + end
  30 +
  31 + module InstanceMethods
  32 +
  33 + protected
  34 +
  35 + def uses_design_blocks?
  36 + !@no_design_blocks && self.class.uses_design_blocks?
  37 + end
  38 +
  39 + def load_custom_design
  40 + # see also: LayoutHelper#body_classes
  41 + @layout_template = self.class.custom_design[:layout_template]
  42 + end
  43 +
  44 + def custom_design
  45 + @custom_design || self.class.custom_design
  46 + end
  47 +
  48 + end
  49 +
  50 +end
... ...
app/helpers/forms_helper.rb
... ... @@ -265,7 +265,7 @@ module FormsHelper
265 265 )
266 266 end
267 267  
268   - def select_profile_folder(label_text, field_id, profile, default_value='', html_options = {}, js_options = {}, find_options = {})
  268 + def select_profile_folder(label_text, field_id, profile, default_value='', html_options = {}, js_options = {}, find_options = {}, extra_options = {})
269 269 if find_options.empty?
270 270 folders = profile.folders
271 271 else
... ... @@ -276,7 +276,7 @@ module FormsHelper
276 276 select_tag(
277 277 field_id,
278 278 options_for_select(
279   - [[profile.identifier, '']] +
  279 + [[(extra_options[:root_label] || profile.identifier), '']] +
280 280 folders.collect {|f| [ profile.identifier + '/' + f.full_name, f.id.to_s ] },
281 281 default_value.to_s
282 282 ),
... ...
app/helpers/language_helper.rb
1 1 module LanguageHelper
2 2 def language
3   - locale
  3 + locale.to_s
4 4 end
5 5  
6 6 def tinymce_language
... ... @@ -20,7 +20,7 @@ module LanguageHelper
20 20 separator = options[:separator] || ' &mdash; '
21 21  
22 22 if options[:element] == 'dropdown'
23   - select_tag('lang',
  23 + select_tag('lang',
24 24 options_for_select(locales.map{|code,name| [name, code]}, current),
25 25 :onchange => "document.location.href= #{url_for(params.merge(:lang => 'LANGUAGE'))}.replace(/LANGUAGE/, this.value) ;",
26 26 :help => _('The language you choose here is the language used for options, buttons, etc. It does not affect the language of the content created by other users.')
... ...
app/helpers/layout_helper.rb
... ... @@ -31,12 +31,12 @@ module LayoutHelper
31 31 plugins_javascripts = @plugins.map { |plugin| [plugin.js_files].flatten.map { |js| plugin.class.public_path(js) } }.flatten
32 32  
33 33 output = ''
34   - output += render :file => 'layouts/_javascript'
35   - output += javascript_tag 'render_all_jquery_ui_widgets()'
  34 + output += render 'layouts/javascript'
36 35 unless plugins_javascripts.empty?
37 36 output += javascript_include_tag plugins_javascripts, :cache => "cache/plugins-#{Digest::MD5.hexdigest plugins_javascripts.to_s}"
38 37 end
39 38 output += theme_javascript_ng.to_s
  39 + output += javascript_tag 'render_all_jquery_ui_widgets()'
40 40  
41 41 output
42 42 end
... ... @@ -45,10 +45,10 @@ module LayoutHelper
45 45 standard_stylesheets = [
46 46 'application',
47 47 'search',
48   - 'thickbox',
49   - 'lightbox',
50 48 'colorbox',
  49 + 'selectordie',
51 50 'inputosaurus',
  51 + 'chat',
52 52 pngfix_stylesheet_path,
53 53 ] + tokeninput_stylesheets
54 54 plugins_stylesheets = @plugins.select(&:stylesheet?).map { |plugin| plugin.class.public_path('style.css') }
... ... @@ -85,6 +85,7 @@ module LayoutHelper
85 85 end
86 86 end
87 87  
  88 +
88 89 def icon_theme_stylesheet_path
89 90 icon_themes = []
90 91 theme_icon_themes = theme_option(:icon_theme) || []
... ... @@ -115,8 +116,5 @@ module LayoutHelper
115 116 end
116 117 end
117 118  
118   - def meta_description_tag(article=nil)
119   - article ? CGI.escapeHTML(truncate(strip_tags(article.body.to_s), :length => 200)) : environment.name
120   - end
121 119 end
122 120  
... ...
app/helpers/lightbox_helper.rb
... ... @@ -1,36 +0,0 @@
1   -module LightboxHelper
2   -
3   - def lightbox_link_to(text, url, options = {})
4   - link_to(text, url, lightbox_options(options))
5   - end
6   -
7   - def lightbox_close_button(text, options = {})
8   - button(:close, text, '#', lightbox_options(options, 'lbAction').merge(:rel => 'deactivate'))
9   - end
10   -
11   - def lightbox_button(type, label, url, options = {})
12   - button(type, label, url, lightbox_options(options))
13   - end
14   -
15   - def lightbox_icon_button(type, label, url, options = {})
16   - icon_button(type, label, url, lightbox_options(options))
17   - end
18   -
19   - # options must be an HTML options hash as passed to link_to etc.
20   - #
21   - # returns a new hash with lightbox class added. Keeps existing classes.
22   - def lightbox_options(options, lightbox_type = 'lbOn')
23   - the_class = lightbox_type
24   - the_class << " #{options[:class]}" if options.has_key?(:class)
25   - options.merge(:class => the_class)
26   - end
27   -
28   - def lightbox?
29   - request.xhr?
30   - end
31   -
32   - def lightbox_remote_button(type, label, url, options = {})
33   - button(type, label, url, lightbox_options(options, 'remote-lbOn'))
34   - end
35   -
36   -end
app/helpers/manage_products_helper.rb
... ... @@ -137,7 +137,7 @@ module ManageProductsHelper
137 137 ui_button_to_remote(label,
138 138 {:update => "product-#{field}",
139 139 :url => { :controller => 'manage_products', :action => "edit", :id => product.id, :field => field },
140   - :complete => "$('edit-product-button-ui-#{field}').hide()",
  140 + :complete => "jQuery('#edit-product-button-ui-#{field}').hide()",
141 141 :method => :get,
142 142 :loading => "loading_for_button('##{id}')"},
143 143 options)
... ...
app/helpers/modal_helper.rb 0 → 100644
... ... @@ -0,0 +1,46 @@
  1 +module ModalHelper
  2 +
  3 + def modal_inline_link_to title, url, selector, options = {}
  4 + link_to title, url, modal_options(options.merge(:inline => selector))
  5 + end
  6 +
  7 + def modal_inline_icon type, title, url, selector, options = {}
  8 + icon_button type, title, url, modal_options(options.merge(:inline => selector))
  9 + end
  10 +
  11 + def modal_link_to title, url, options = {}
  12 + link_to title, url, modal_options(options)
  13 + end
  14 +
  15 + def modal_close_link text, options = {}
  16 + link_to text, '#', modal_options(options, :close)
  17 + end
  18 +
  19 + def modal_close_button(text, options = {})
  20 + button :close, text, '#', modal_options(options, :close).merge(:rel => 'deactivate')
  21 + end
  22 +
  23 + def modal_button(type, label, url, options = {})
  24 + button type, label, url, modal_options(options)
  25 + end
  26 +
  27 + def modal_icon_button(type, label, url, options = {})
  28 + icon_button type, label, url, modal_options(options)
  29 + end
  30 +
  31 + # options must be an HTML options hash as passed to link_to etc.
  32 + #
  33 + # returns a new hash with modal class added. Keeps existing classes.
  34 + def modal_options(options, type=nil)
  35 + inline_selector = options.delete :inline
  36 + options[:onclick] = "return noosfero.modal.inline('#{inline_selector}')" if inline_selector
  37 +
  38 + classes = if inline_selector then '' else 'modal-toggle' end
  39 + classes += " modal-#{type.to_s}" if type.present?
  40 + classes << " #{options[:class]}" if options.has_key? :class
  41 + options.merge!(:class => classes)
  42 +
  43 + options
  44 + end
  45 +
  46 +end
... ...
app/helpers/person_notifier_helper.rb
... ... @@ -1,15 +0,0 @@
1   -module PersonNotifierHelper
2   -
3   - include ApplicationHelper
4   -
5   - private
6   -
7   - def path_to_image(source)
8   - top_url + source
9   - end
10   -
11   - def top_url
12   - top_url = @profile.environment ? @profile.environment.top_url : ''
13   - end
14   -
15   -end
app/helpers/plugins_helper.rb 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +module PluginsHelper
  2 +
  3 + def plugins_product_tabs
  4 + @plugins.dispatch(:product_tabs, @product).map do |tab|
  5 + {:title => tab[:title], :id => tab[:id], :content => instance_eval(&tab[:content])}
  6 + end
  7 + end
  8 +
  9 +end
... ...
app/helpers/search_helper.rb
... ... @@ -5,20 +5,23 @@ module SearchHelper
5 5 BLOCKS_SEARCH_LIMIT = 24
6 6 MULTIPLE_SEARCH_LIMIT = 8
7 7  
8   - SEARCHES = ActiveSupport::OrderedHash[
9   - :articles, _('Contents'),
10   - :enterprises, _('Enterprises'),
11   - :people, _('People'),
12   - :communities, _('Communities'),
13   - :products, _('Products and Services'),
14   - :events, _('Events'),
15   - ]
  8 + FILTERS_TRANSLATIONS = {
  9 + :order => _('Order'),
  10 + :display => _('Display')
  11 + }
16 12  
17   - FILTER_TRANSLATION = {
18   - 'more_popular' => _('More popular'),
19   - 'more_active' => _('More active'),
20   - 'more_recent' => _('More recent'),
21   - 'more_comments' => _('More comments')
  13 + FILTERS_OPTIONS_TRANSLATION = {
  14 + :order => {
  15 + 'more_popular' => _('More popular'),
  16 + 'more_active' => _('More active'),
  17 + 'more_recent' => _('More recent'),
  18 + 'more_comments' => _('More comments')
  19 + },
  20 + :display => {
  21 + 'map' => _('Map'),
  22 + 'full' => _('Full'),
  23 + 'compact' => _('Compact')
  24 + }
22 25 }
23 26  
24 27 COMMON_PROFILE_LIST_BLOCK = [
... ... @@ -56,7 +59,7 @@ module SearchHelper
56 59 end
57 60  
58 61 def display?(asset, mode)
59   - defined?(asset_class(asset)::SEARCH_DISPLAYS) && asset_class(asset)::SEARCH_DISPLAYS.include?(mode.to_s)
  62 + defined?(asset_class(asset)::SEARCH_FILTERS[:display]) && asset_class(asset)::SEARCH_FILTERS[:display].include?(mode.to_s)
60 63 end
61 64  
62 65 def display_results(searches=nil, asset=nil)
... ... @@ -93,6 +96,16 @@ module SearchHelper
93 96 end
94 97 end
95 98  
  99 + def select_filter(name, options, default = nil)
  100 + if options.size <= 1
  101 + return
  102 + else
  103 + options = options.map {|option| [FILTERS_OPTIONS_TRANSLATION[name][option], option]}
  104 + options = options_for_select(options, :selected => (params[name] || default))
  105 + select_tag(name, options)
  106 + end
  107 + end
  108 +
96 109 def display_selector(asset, display, float = 'right')
97 110 display = nil if display.blank?
98 111 display ||= asset_class(asset).default_search_display
... ... @@ -107,36 +120,32 @@ module SearchHelper
107 120 end
108 121 end
109 122  
110   - def filter_selector(asset, filter, float = 'right')
  123 + def filters(asset)
  124 + return if !asset
111 125 klass = asset_class(asset)
112   - if klass::SEARCH_FILTERS.count > 1
113   - options = options_for_select(klass::SEARCH_FILTERS.map {|f| [FILTER_TRANSLATION[f], f]}, filter)
114   - url_params = url_for(params.merge(:filter => 'FILTER'))
115   - onchange = "document.location.href = '#{url_params}'.replace('FILTER', this.value)"
116   - select_field = select_tag(:filter, options, :onchange => onchange)
117   - content_tag('div',
118   - content_tag('strong', _('Filter')) + ': ' + select_field,
119   - :class => "search-customize-options"
120   - )
121   - end
  126 + content_tag('div', klass::SEARCH_FILTERS.map do |name, options|
  127 + default = klass.respond_to?("default_search_#{name}") ? klass.send("default_search_#{name}".to_s) : nil
  128 + select_filter(name, options, default)
  129 + end.join("\n"), :id => 'search-filters')
  130 + end
  131 +
  132 + def assets_menu(selected)
  133 + assets = @enabled_searches.keys
  134 + # Events is a search asset but do not have a good interface for
  135 + #TODO searching. When this is solved we may add it back again to the assets
  136 + # menu.
  137 + assets.delete(:events)
  138 + content_tag('ul',
  139 + assets.map do |asset|
  140 + options = {}
  141 + options.merge!(:class => 'selected') if selected.to_s == asset.to_s
  142 + content_tag('li', asset_link(asset), options)
  143 + end.join("\n"),
  144 + :id => 'assets-menu')
122 145 end
123 146  
124   - def filter_title(asset, filter)
125   - {
126   - 'articles_more_recent' => _('More recent contents from network'),
127   - 'articles_more_popular' => _('More viewed contents from network'),
128   - 'articles_more_comments' => _('Most commented contents from network'),
129   - 'people_more_recent' => _('More recent people from network'),
130   - 'people_more_active' => _('More active people from network'),
131   - 'people_more_popular' => _('More popular people from network'),
132   - 'communities_more_recent' => _('More recent communities from network'),
133   - 'communities_more_active' => _('More active communities from network'),
134   - 'communities_more_popular' => _('More popular communities from network'),
135   - 'enterprises_more_recent' => _('More recent enterprises from network'),
136   - 'enterprises_more_active' => _('More active enterprises from network'),
137   - 'enterprises_more_popular' => _('More popular enterprises from network'),
138   - 'products_more_recent' => _('Highlights'),
139   - }[asset.to_s + '_' + filter].to_s
  147 + def asset_link(asset)
  148 + link_to(@enabled_searches[asset], "/search/#{asset}")
140 149 end
141 150  
142 151 end
... ...
app/helpers/search_term_helper.rb 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +module SearchTermHelper
  2 + def register_search_term(term, total, indexed, context, asset='all')
  3 + normalized_term = normalize_term(term)
  4 + if normalized_term.present?
  5 + search_term = SearchTerm.find_or_create(normalized_term, context, asset)
  6 + SearchTermOccurrence.create!(:search_term => search_term, :total => total, :indexed => indexed)
  7 + end
  8 + end
  9 +
  10 + #FIXME For some reason the job is created but nothing is ran.
  11 + #handle_asynchronously :register_search_term
  12 +
  13 + #TODO Think smarter criteria to normalize search terms properly
  14 + def normalize_term(search_term)
  15 + search_term ||= ''
  16 + search_term.downcase
  17 + end
  18 +end
... ...
app/helpers/thickbox_helper.rb
... ... @@ -1,11 +0,0 @@
1   -module ThickboxHelper
2   - def thickbox_inline_popup_link(title, url, id, options = {})
3   - link_to(title, url_for(url) + "#TB_inline?height=300&width=500&inlineId=#{id}&modal=true", {:class => 'thickbox'}.merge(options))
4   - end
5   - def thickbox_inline_popup_icon(type, title, url, id, options = {})
6   - icon_button(type, title, url_for(url) + "#TB_inline?height=300&width=500&inlineId=#{id}&modal=true", {:class => "thickbox"}.merge(options))
7   - end
8   - def thickbox_close_button(title)
9   - button_to_function(:close, title, 'tb_remove();')
10   - end
11   -end
app/helpers/tinymce_helper.rb
... ... @@ -11,7 +11,7 @@ module TinymceHelper
11 11 end
12 12  
13 13 def tinymce_init_js options = {}
14   - options.merge! :document_base_url => environment.top_url,
  14 + options.merge! :document_base_url => top_url,
15 15 :content_css => "/stylesheets/tinymce.css,#{macro_css_files}",
16 16 :plugins => %w[compat3x advlist autolink lists link image charmap print preview hr anchor pagebreak
17 17 searchreplace wordcount visualblocks visualchars code fullscreen
... ...
app/helpers/token_helper.rb
... ... @@ -12,6 +12,7 @@ module TokenHelper
12 12 options[:search_delay] ||= 1000
13 13 options[:prevent_duplicates] ||= true
14 14 options[:backspace_delete_item] ||= false
  15 + options[:zindex] ||= 999
15 16 options[:focus] ||= false
16 17 options[:avoid_enter] ||= true
17 18 options[:on_result] ||= 'null'
... ... @@ -31,6 +32,7 @@ module TokenHelper
31 32 searchDelay: #{options[:search_delay].to_json},
32 33 preventDuplicates: #{options[:prevent_duplicates].to_json},
33 34 backspaceDeleteItem: #{options[:backspace_delete_item].to_json},
  35 + zindex: #{options[:zindex].to_json},
34 36 queryParam: #{options[:query_param].to_json},
35 37 tokenLimit: #{options[:token_limit].to_json},
36 38 onResult: #{options[:on_result]},
... ...
app/mailers/comment_notifier.rb
1   -class Comment::Notifier < ActionMailer::Base
  1 +class CommentNotifier < ActionMailer::Base
2 2 def notification(comment)
3 3 profile = comment.article.profile
4 4 @recipient = profile.nickname || profile.name
... ...
app/mailers/scrap_notifier.rb
1   -class Scrap::Notifier < ActionMailer::Base
  1 +class ScrapNotifier < ActionMailer::Base
2 2 def notification(scrap)
3 3 sender, receiver = scrap.sender, scrap.receiver
4 4 @recipient = receiver.name
... ...
app/mailers/user_mailer.rb
... ... @@ -41,6 +41,23 @@ class UserMailer &lt; ActionMailer::Base
41 41 )
42 42 end
43 43  
  44 + def profiles_suggestions_email(user)
  45 + @recipient = user.name
  46 + @environment = user.environment.name
  47 + @url = user.environment.top_url
  48 + @people_suggestions_url = user.people_suggestions_url
  49 + @people_suggestions = user.suggested_people.sample(3)
  50 + @communities_suggestions_url = user.communities_suggestions_url
  51 + @communities_suggestions = user.suggested_communities.sample(3)
  52 +
  53 + mail(
  54 + content_type: 'text/html',
  55 + to: user.email,
  56 + from: "#{user.environment.name} <#{user.environment.contact_email}>",
  57 + subject: _("[%s] What about grow up your network?") % user.environment.name
  58 + )
  59 + end
  60 +
44 61 class Job < Struct.new(:user, :method)
45 62 def perform
46 63 UserMailer.send(method, user).deliver
... ...
app/models/add_friend.rb
... ... @@ -14,6 +14,11 @@ class AddFriend &lt; Task
14 14 alias :friend :target
15 15 alias :friend= :target=
16 16  
  17 + after_create do |task|
  18 + TaskMailer.invitation_notification(task).deliver unless task.friend
  19 + remove_from_suggestion_list(task)
  20 + end
  21 +
17 22 def perform
18 23 target.add_friend(requestor, group_for_friend)
19 24 requestor.add_friend(target, group_for_person)
... ... @@ -48,4 +53,8 @@ class AddFriend &lt; Task
48 53 {:type => :profile_image, :profile => requestor, :url => requestor.url}
49 54 end
50 55  
  56 + def remove_from_suggestion_list(task)
  57 + suggestion = task.requestor.profile_suggestions.find_by_suggestion_id task.target.id
  58 + suggestion.disable if suggestion
  59 + end
51 60 end
... ...
app/models/add_member.rb
... ... @@ -10,6 +10,10 @@ class AddMember &lt; Task
10 10  
11 11 settings_items :roles
12 12  
  13 + after_create do |task|
  14 + remove_from_suggestion_list(task)
  15 + end
  16 +
13 17 def perform
14 18 if !self.roles or (self.roles.uniq.compact.length == 1 and self.roles.uniq.compact.first.to_i.zero?)
15 19 self.roles = [Profile::Roles.member(organization.environment.id).id]
... ... @@ -46,4 +50,9 @@ class AddMember &lt; Task
46 50 _('You will need login to %{system} in order to accept or reject %{requestor} as a member of %{organization}.') % { :system => target.environment.name, :requestor => requestor.name, :organization => organization.name }
47 51 end
48 52  
  53 + def remove_from_suggestion_list(task)
  54 + suggestion = task.requestor.profile_suggestions.find_by_suggestion_id task.target.id
  55 + suggestion.disable if suggestion
  56 + end
  57 +
49 58 end
... ...
app/models/approve_article.rb
... ... @@ -22,6 +22,7 @@ class ApproveArticle &lt; Task
22 22 end
23 23  
24 24 settings_items :closing_statment, :article_parent_id, :highlighted
  25 + settings_items :create_link, :type => :boolean, :default => false
25 26  
26 27 def article_parent
27 28 Article.find_by_id article_parent_id.to_i
... ... @@ -48,7 +49,11 @@ class ApproveArticle &lt; Task
48 49 end
49 50  
50 51 def perform
51   - article.copy!(:name => name, :abstract => abstract, :body => body, :profile => target, :reference_article => article, :parent => article_parent, :highlighted => highlighted, :source => article.source, :last_changed_by_id => article.last_changed_by_id, :created_by_id => article.created_by_id)
  52 + if create_link
  53 + LinkArticle.create!(:reference_article => article, :profile => target, :parent => article_parent, :highlighted => highlighted)
  54 + else
  55 + article.copy!(:name => name, :abstract => abstract, :body => body, :profile => target, :reference_article => article, :parent => article_parent, :highlighted => highlighted, :source => article.source, :last_changed_by_id => article.last_changed_by_id, :created_by_id => article.created_by_id)
  56 + end
52 57 end
53 58  
54 59 def title
... ...
app/models/article.rb
1   -require 'hpricot'
2 1  
3 2 class Article < ActiveRecord::Base
4 3  
... ... @@ -14,20 +13,17 @@ class Article &lt; ActiveRecord::Base
14 13 acts_as_having_image
15 14  
16 15 SEARCHABLE_FIELDS = {
17   - :name => 10,
18   - :abstract => 3,
19   - :body => 2,
20   - :slug => 1,
21   - :filename => 1,
  16 + :name => {:label => _('Name'), :weight => 10},
  17 + :abstract => {:label => _('Abstract'), :weight => 3},
  18 + :body => {:label => _('Content'), :weight => 2},
  19 + :slug => {:label => _('Slug'), :weight => 1},
  20 + :filename => {:label => _('Filename'), :weight => 1},
22 21 }
23 22  
24   - SEARCH_FILTERS = %w[
25   - more_recent
26   - more_popular
27   - more_comments
28   - ]
29   -
30   - SEARCH_DISPLAYS = %w[full]
  23 + SEARCH_FILTERS = {
  24 + :order => %w[more_recent more_popular more_comments],
  25 + :display => %w[full]
  26 + }
31 27  
32 28 def self.default_search_display
33 29 'full'
... ... @@ -110,6 +106,11 @@ class Article &lt; ActiveRecord::Base
110 106 self.activity.destroy if self.activity
111 107 end
112 108  
  109 + after_destroy :destroy_link_article
  110 + def destroy_link_article
  111 + Article.where(:reference_article_id => self.id, :type => LinkArticle).destroy_all
  112 + end
  113 +
113 114 xss_terminate :only => [ :name ], :on => 'validation', :with => 'white_list'
114 115  
115 116 scope :in_category, lambda { |category|
... ... @@ -388,6 +389,10 @@ class Article &lt; ActiveRecord::Base
388 389 {}
389 390 end
390 391  
  392 + def alternate_languages
  393 + self.translations.map(&:language)
  394 + end
  395 +
391 396 scope :native_translations, :conditions => { :translation_of_id => nil }
392 397  
393 398 def translatable?
... ... @@ -472,7 +477,9 @@ class Article &lt; ActiveRecord::Base
472 477 scope :no_folders, lambda {|profile|{:conditions => ['articles.type NOT IN (?)', profile.folder_types]}}
473 478 scope :galleries, :conditions => [ "articles.type IN ('Gallery')" ]
474 479 scope :images, :conditions => { :is_image => true }
  480 + scope :no_images, :conditions => { :is_image => false }
475 481 scope :text_articles, :conditions => [ 'articles.type IN (?)', text_article_types ]
  482 + scope :files, :conditions => { :type => 'UploadedFile' }
476 483 scope :with_types, lambda { |types| { :conditions => [ 'articles.type IN (?)', types ] } }
477 484  
478 485 scope :more_popular, :order => 'hits DESC'
... ... @@ -523,7 +530,10 @@ class Article &lt; ActiveRecord::Base
523 530 end
524 531  
525 532 alias :allow_delete? :allow_post_content?
526   - alias :allow_spread? :allow_post_content?
  533 +
  534 + def allow_spread?(user = nil)
  535 + user && public?
  536 + end
527 537  
528 538 def allow_create?(user)
529 539 allow_post_content?(user) || allow_publish_content?(user)
... ... @@ -696,7 +706,7 @@ class Article &lt; ActiveRecord::Base
696 706 end
697 707  
698 708 def first_paragraph
699   - paragraphs = Hpricot(to_html).search('p')
  709 + paragraphs = Nokogiri::HTML.fragment(to_html).css('p')
700 710 paragraphs.empty? ? '' : paragraphs.first.to_html
701 711 end
702 712  
... ... @@ -718,8 +728,8 @@ class Article &lt; ActiveRecord::Base
718 728  
719 729 def body_images_paths
720 730 require 'uri'
721   - Hpricot(self.body.to_s).search('img[@src]').collect do |i|
722   - (self.profile && self.profile.environment) ? URI.join(self.profile.environment.top_url, URI.escape(i.attributes['src'])).to_s : i.attributes['src']
  731 + Nokogiri::HTML.fragment(self.body.to_s).css('img[src]').collect do |i|
  732 + (self.profile && self.profile.environment) ? URI.join(self.profile.environment.top_url, URI.escape(i['src'])).to_s : i['src']
723 733 end
724 734 end
725 735  
... ... @@ -756,11 +766,11 @@ class Article &lt; ActiveRecord::Base
756 766 end
757 767  
758 768 def first_image
759   - img = Hpricot(self.lead.to_s).search('img[@src]').first || Hpricot(self.body.to_s).search('img').first
760   - img.nil? ? '' : img.attributes['src']
  769 + img = Nokogiri::HTML.fragment(self.lead.to_s).css('img[src]').first || Nokogiri::HTML.fragment(self.body.to_s).search('img').first
  770 + img.nil? ? '' : img['src']
761 771 end
762 772  
763   - delegate :region, :region_id, :environment, :environment_id, :to => :profile, :allow_nil => true
  773 + delegate :lat, :lng, :region, :region_id, :environment, :environment_id, :to => :profile, :allow_nil => true
764 774  
765 775 def has_macro?
766 776 true
... ...
app/models/blog_archives_block.rb
... ... @@ -36,8 +36,7 @@ class BlogArchivesBlock &lt; Block
36 36 results << content_tag('li', content_tag('strong', "#{year} (#{count})"))
37 37 results << "<ul class='#{year}-archive'>"
38 38 posts.except(:order).count(:all, :conditions => ['EXTRACT(YEAR FROM published_at)=?', year], :group => 'EXTRACT(MONTH FROM published_at)').sort_by {|month, count| -month.to_i}.each do |month, count|
39   - month_name = gettext(MONTHS[month.to_i - 1])
40   - results << content_tag('li', link_to("#{month_name} (#{count})", owner_blog.url.merge(:year => year, :month => month)))
  39 + results << content_tag('li', link_to("#{month_name(month.to_i)} (#{count})", owner_blog.url.merge(:year => year, :month => month)))
41 40 end
42 41 results << "</ul>"
43 42 end
... ...
app/models/box.rb
... ... @@ -14,8 +14,8 @@ class Box &lt; ActiveRecord::Base
14 14 end
15 15  
16 16 def acceptable_blocks
17   - blocks_classes = central? ? Box.acceptable_center_blocks + plugins.dispatch(:extra_blocks, :type => owner.class, :position => 1) : Box.acceptable_side_blocks + plugins.dispatch(:extra_blocks, :type => owner.class, :position => [2, 3])
18   - to_css_class_name(blocks_classes)
  17 + blocks_classes = if central? then Box.acceptable_center_blocks + plugins.dispatch(:extra_blocks, :type => owner.class, :position => 1) else Box.acceptable_side_blocks + plugins.dispatch(:extra_blocks, :type => owner.class, :position => [2, 3]) end
  18 + to_css_selector blocks_classes
19 19 end
20 20  
21 21 def central?
... ... @@ -74,8 +74,8 @@ class Box &lt; ActiveRecord::Base
74 74  
75 75 private
76 76  
77   - def to_css_class_name(blocks_classes)
78   - blocks_classes.map{ |block_class| block_class.name.to_css_class }
  77 + def to_css_selector(blocks_classes)
  78 + blocks_classes.map{ |block_class| ".#{block_class.name.to_css_class}" }.join(',')
79 79 end
80 80  
81 81 end
... ...
app/models/category.rb
... ... @@ -3,10 +3,10 @@ class Category &lt; ActiveRecord::Base
3 3 attr_accessible :name, :parent_id, :display_color, :display_in_menu, :image_builder, :environment, :parent
4 4  
5 5 SEARCHABLE_FIELDS = {
6   - :name => 10,
7   - :acronym => 5,
8   - :abbreviation => 5,
9   - :slug => 1,
  6 + :name => {:label => _('Name'), :weight => 10},
  7 + :acronym => {:label => _('Acronym'), :weight => 5},
  8 + :abbreviation => {:label => _('Abbreviation'), :weight => 5},
  9 + :slug => {:label => _('Slug'), :weight => 1},
10 10 }
11 11  
12 12 validates_exclusion_of :slug, :in => [ 'index' ], :message => N_('{fn} cannot be like that.').fix_i18n
... ...
app/models/certifier.rb
... ... @@ -3,9 +3,9 @@ class Certifier &lt; ActiveRecord::Base
3 3 attr_accessible :name, :environment
4 4  
5 5 SEARCHABLE_FIELDS = {
6   - :name => 10,
7   - :description => 3,
8   - :link => 1,
  6 + :name => {:label => _('Name'), :weight => 10},
  7 + :description => {:label => _('Description'), :weight => 3},
  8 + :link => {:label => _('Link'), :weight => 1},
9 9 }
10 10  
11 11 belongs_to :environment
... ...
app/models/chat_message.rb 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +class ChatMessage < ActiveRecord::Base
  2 + attr_accessible :body, :from, :to
  3 +
  4 + belongs_to :to, :class_name => 'Profile'
  5 + belongs_to :from, :class_name => 'Profile'
  6 +
  7 +end
... ...
app/models/comment.rb
1 1 class Comment < ActiveRecord::Base
2 2  
3 3 SEARCHABLE_FIELDS = {
4   - :title => 10,
5   - :name => 4,
6   - :body => 2,
  4 + :title => {:label => _('Title'), :weight => 10},
  5 + :name => {:label => _('Name'), :weight => 4},
  6 + :body => {:label => _('Content'), :weight => 2},
7 7 }
8 8  
9 9 attr_accessible :body, :author, :name, :email, :title, :reply_of_id, :source
... ... @@ -134,11 +134,11 @@ class Comment &lt; ActiveRecord::Base
134 134 def notify_by_mail
135 135 if source.kind_of?(Article) && article.notify_comments?
136 136 if !notification_emails.empty?
137   - Comment::Notifier.notification(self).deliver
  137 + CommentNotifier.notification(self).deliver
138 138 end
139 139 emails = article.followers - [author_email]
140 140 if !emails.empty?
141   - Comment::Notifier.mail_to_followers(self, emails).deliver
  141 + CommentNotifier.mail_to_followers(self, emails).deliver
142 142 end
143 143 end
144 144 end
... ...
app/models/communities_block.rb
... ... @@ -14,19 +14,17 @@ class CommunitiesBlock &lt; ProfileListBlock
14 14 _('This block displays the communities in which the user is a member.')
15 15 end
16 16  
  17 + def suggestions
  18 + return nil unless owner.kind_of?(Profile)
  19 + owner.profile_suggestions.of_community.enabled.limit(3).includes(:suggestion)
  20 + end
  21 +
17 22 def footer
18 23 owner = self.owner
19   - case owner
20   - when Profile
21   - lambda do |context|
22   - link_to s_('communities|View all'), :profile => owner.identifier, :controller => 'profile', :action => 'communities'
23   - end
24   - when Environment
25   - lambda do |context|
26   - link_to s_('communities|View all'), :controller => 'search', :action => 'communities'
27   - end
28   - else
29   - ''
  24 + suggestions = self.suggestions
  25 + return '' unless owner.kind_of?(Profile) || owner.kind_of?(Environment)
  26 + proc do
  27 + render :file => 'blocks/communities', :locals => { :owner => owner, :suggestions => suggestions }
30 28 end
31 29 end
32 30  
... ...
app/models/create_enterprise.rb
... ... @@ -73,7 +73,13 @@ class CreateEnterprise &lt; Task
73 73  
74 74 # sets the associated region for the enterprise creation
75 75 def region=(value)
76   - raise ArgumentError.new("Region expected, but got #{value.class}") unless value.kind_of?(Region)
  76 + unless value.kind_of?(Region)
  77 + begin
  78 + value = Region.find(value)
  79 + rescue
  80 + raise ArgumentError.new("Could not find any region with the id #{value}")
  81 + end
  82 + end
77 83  
78 84 @region = value
79 85 self.region_id = value.id
... ...
app/models/enterprise.rb
... ... @@ -4,7 +4,10 @@ class Enterprise &lt; Organization
4 4  
5 5 attr_accessible :business_name, :address_reference, :district, :tag_list, :organization_website, :historic_and_current_context, :activities_short_description, :products_per_catalog_page
6 6  
7   - SEARCH_DISPLAYS += %w[map full]
  7 + SEARCH_FILTERS = {
  8 + :order => %w[more_recent more_popular more_active],
  9 + :display => %w[compact full map]
  10 + }
8 11  
9 12 def self.type_name
10 13 _('Enterprise')
... ... @@ -16,7 +19,8 @@ class Enterprise &lt; Organization
16 19 has_many :inputs, :through => :products
17 20 has_many :production_costs, :as => :owner
18 21  
19   - has_and_belongs_to_many :fans, :class_name => 'Person', :join_table => 'favorite_enteprises_people'
  22 + has_many :favorite_enterprise_people
  23 + has_many :fans, through: :favorite_enterprise_people, source: :person
20 24  
21 25 def product_categories
22 26 ProductCategory.by_enterprise(self)
... ...
app/models/environment.rb
... ... @@ -3,13 +3,14 @@
3 3 # domains.
4 4 class Environment < ActiveRecord::Base
5 5  
6   - attr_accessible :name, :is_default, :signup_welcome_text_subject, :signup_welcome_text_body, :terms_of_use, :message_for_disabled_enterprise, :news_amount_by_folder, :default_language, :languages, :description, :organization_approval_method, :enabled_plugins, :enabled_features, :redirection_after_login, :redirection_after_signup, :contact_email, :theme, :reports_lower_bound, :noreply_email, :signup_welcome_screen_body, :members_whitelist_enabled, :members_whitelist
  6 + attr_accessible :name, :is_default, :signup_welcome_text_subject, :signup_welcome_text_body, :terms_of_use, :message_for_disabled_enterprise, :news_amount_by_folder, :default_language, :languages, :description, :organization_approval_method, :enabled_plugins, :enabled_features, :redirection_after_login, :redirection_after_signup, :contact_email, :theme, :reports_lower_bound, :noreply_email, :signup_welcome_screen_body, :members_whitelist_enabled, :members_whitelist, :highlighted_news_amount, :portal_news_amount
7 7  
8 8 has_many :users
9 9  
10 10 self.partial_updates = false
11 11  
12 12 has_many :tasks, :dependent => :destroy, :as => 'target'
  13 + has_many :search_terms, :as => :context
13 14  
14 15 IDENTIFY_SCRIPTS = /(php[0-9s]?|[sp]htm[l]?|pl|py|cgi|rb)/
15 16  
... ... @@ -85,7 +86,9 @@ class Environment &lt; ActiveRecord::Base
85 86 end
86 87  
87 88 def admins
88   - Person.members_of(self).all(:conditions => ['role_assignments.role_id = ?', Environment::Roles.admin(self).id])
  89 + admin_role = Environment::Roles.admin(self)
  90 + return [] if admin_role.blank?
  91 + Person.members_of(self).all(:conditions => ['role_assignments.role_id = ?', admin_role.id])
89 92 end
90 93  
91 94 # returns the available features for a Environment, in the form of a
... ... @@ -155,7 +158,8 @@ class Environment &lt; ActiveRecord::Base
155 158 'site_homepage' => _('Redirects the user to the environment homepage.'),
156 159 'user_profile_page' => _('Redirects the user to his profile page.'),
157 160 'user_homepage' => _('Redirects the user to his homepage.'),
158   - 'user_control_panel' => _('Redirects the user to his control panel.')
  161 + 'user_control_panel' => _('Redirects the user to his control panel.'),
  162 + 'welcome_page' => _('Redirects the user to the environment welcome page.')
159 163 }
160 164 end
161 165 validates_inclusion_of :redirection_after_signup, :in => Environment.signup_redirection_options.keys, :allow_nil => true
... ... @@ -264,9 +268,12 @@ class Environment &lt; ActiveRecord::Base
264 268 settings_items :description, :type => String, :default => '<div style="text-align: center"><a href="http://noosfero.org/"><img src="/images/noosfero-network.png" alt="Noosfero"/></a></div>'
265 269 settings_items :local_docs, :type => Array, :default => []
266 270 settings_items :news_amount_by_folder, :type => Integer, :default => 4
  271 + settings_items :highlighted_news_amount, :type => Integer, :default => 2
  272 + settings_items :portal_news_amount, :type => Integer, :default => 5
267 273 settings_items :help_message_to_add_enterprise, :type => String, :default => ''
268 274 settings_items :tip_message_enterprise_activation_question, :type => String, :default => ''
269 275  
  276 + settings_items :currency_iso_unit, :type => String, :default => 'USD'
270 277 settings_items :currency_unit, :type => String, :default => '$'
271 278 settings_items :currency_separator, :type => String, :default => '.'
272 279 settings_items :currency_delimiter, :type => String, :default => ','
... ... @@ -657,8 +664,8 @@ class Environment &lt; ActiveRecord::Base
657 664 { :controller => 'admin_panel', :action => 'index' }
658 665 end
659 666  
660   - def top_url
661   - url = 'http://'
  667 + def top_url(scheme = 'http')
  668 + url = scheme + '://'
662 669 url << (Noosfero.url_options.key?(:host) ? Noosfero.url_options[:host] : default_hostname)
663 670 url << ':' << Noosfero.url_options[:port].to_s if Noosfero.url_options.key?(:port)
664 671 url << Noosfero.root('')
... ... @@ -824,6 +831,10 @@ class Environment &lt; ActiveRecord::Base
824 831 "home-page-news/#{cache_key}-#{language}"
825 832 end
826 833  
  834 + def portal_enabled
  835 + portal_community && enabled?('use_portal_community')
  836 + end
  837 +
827 838 def notification_emails
828 839 [contact_email].select(&:present?) + admins.map(&:email)
829 840 end
... ... @@ -937,6 +948,10 @@ class Environment &lt; ActiveRecord::Base
937 948 locales_list
938 949 end
939 950  
  951 + def has_license?
  952 + self.licenses.any?
  953 + end
  954 +
940 955 private
941 956  
942 957 def default_language_available
... ...
app/models/event.rb
... ... @@ -19,7 +19,7 @@ class Event &lt; Article
19 19 maybe_add_http(self.setting[:link])
20 20 end
21 21  
22   - xss_terminate :only => [ :body, :link, :address ], :with => 'white_list', :on => 'validation'
  22 + xss_terminate :only => [ :name, :body, :link, :address ], :with => 'white_list', :on => 'validation'
23 23  
24 24 def initialize(*args)
25 25 super(*args)
... ... @@ -141,6 +141,10 @@ class Event &lt; Article
141 141 result
142 142 end
143 143  
  144 + def duration
  145 + ((self.end_date || self.start_date) - self.start_date).to_i
  146 + end
  147 +
144 148 def lead
145 149 content_tag('div',
146 150 show_period(start_date, end_date),
... ...
app/models/external_feed.rb
... ... @@ -14,9 +14,9 @@ class ExternalFeed &lt; ActiveRecord::Base
14 14  
15 15 def add_item(title, link, date, content)
16 16 return if content.blank?
17   - doc = Hpricot(content)
18   - doc.search('*').each do |p|
19   - if p.instance_of? Hpricot::Elem
  17 + doc = Nokogiri::HTML.fragment content
  18 + doc.css('*').each do |p|
  19 + if p.instance_of? Nokogiri::XML::Element
20 20 p.remove_attribute 'style'
21 21 p.remove_attribute 'class'
22 22 end
... ... @@ -26,10 +26,10 @@ class ExternalFeed &lt; ActiveRecord::Base
26 26 article = TinyMceArticle.new
27 27 article.name = title
28 28 article.profile = blog.profile
29   - article.body = content
30   - article.published_at = date
31   - article.source = link
32   - article.profile = blog.profile
  29 + article.body = content
  30 + article.published_at = date
  31 + article.source = link
  32 + article.profile = blog.profile
33 33 article.parent = blog
34 34 article.author_name = self.feed_title
35 35 unless blog.children.exists?(:slug => article.slug)
... ...
app/models/favorite_enterprise_person.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +class FavoriteEnterprisePerson < ActiveRecord::Base
  2 +
  3 + self.table_name = :favorite_enteprises_people
  4 +
  5 + belongs_to :enterprise
  6 + belongs_to :person
  7 +
  8 +end
... ...
app/models/folder.rb
... ... @@ -12,7 +12,7 @@ class Folder &lt; Article
12 12  
13 13 acts_as_having_settings :field => :setting
14 14  
15   - xss_terminate :only => [ :body ], :with => 'white_list', :on => 'validation'
  15 + xss_terminate :only => [ :name, :body ], :with => 'white_list', :on => 'validation'
16 16  
17 17 include WhiteListFilter
18 18 filter_iframes :body
... ...
app/models/forum.rb
... ... @@ -54,7 +54,7 @@ class Forum &lt; Folder
54 54  
55 55 def first_paragraph
56 56 return '' if body.blank?
57   - paragraphs = Hpricot(body).search('p')
  57 + paragraphs = Nokogiri::HTML.fragment(body).css('p')
58 58 paragraphs.empty? ? '' : paragraphs.first.to_html
59 59 end
60 60  
... ...
app/models/input.rb
1 1 class Input < ActiveRecord::Base
2 2  
3   - attr_accessible :product, :product_category, :product_category_id, :amount_used, :unit_id, :price_per_unit, :relevant_to_price
  3 + attr_accessible :product, :product_id, :product_category, :product_category_id,
  4 + :amount_used, :unit_id, :price_per_unit, :relevant_to_price, :is_from_solidarity_economy
4 5  
5 6 belongs_to :product
6 7 belongs_to :product_category
... ...
app/models/invitation.rb
... ... @@ -51,7 +51,10 @@ class Invitation &lt; Task
51 51 next if contact_to_invite == _("Firstname Lastname <friend@email.com>")
52 52  
53 53 contact_to_invite.strip!
54   - if match = contact_to_invite.match(/(.*)<(.*)>/) and match[2].match(Noosfero::Constants::EMAIL_FORMAT)
  54 + find_by_profile_id = false
  55 + if contact_to_invite.match(/^\d*$/)
  56 + find_by_profile_id = true
  57 + elsif match = contact_to_invite.match(/(.*)<(.*)>/) and match[2].match(Noosfero::Constants::EMAIL_FORMAT)
55 58 friend_name = match[1].strip
56 59 friend_email = match[2]
57 60 elsif match = contact_to_invite.strip.match(Noosfero::Constants::EMAIL_FORMAT)
... ... @@ -61,11 +64,15 @@ class Invitation &lt; Task
61 64 next
62 65 end
63 66  
64   - user = User.find_by_email(friend_email)
  67 + begin
  68 + user = find_by_profile_id ? Person.find_by_id(contact_to_invite).user : User.find_by_email(friend_email)
  69 + rescue
  70 + user = nil
  71 + end
65 72  
66   - task_args = if user.nil?
  73 + task_args = if user.nil? && !find_by_profile_id
67 74 {:person => person, :friend_name => friend_name, :friend_email => friend_email, :message => message}
68   - else
  75 + elsif user.present? && !(user.person.is_a_friend?(person) && profile.person?)
69 76 {:person => person, :target => user.person}
70 77 end
71 78  
... ...
app/models/invite_friend.rb
1 1 class InviteFriend < Invitation
2 2  
3 3 settings_items :group_for_person, :group_for_friend
  4 + before_create :check_for_invitation_existence
4 5  
5 6 def perform
6 7 person.add_friend(friend, group_for_person)
... ... @@ -41,4 +42,11 @@ class InviteFriend &lt; Invitation
41 42 ].join("\n\n")
42 43 end
43 44  
  45 + private
  46 + def check_for_invitation_existence
  47 + if friend
  48 + friend.tasks.pending.of("InviteFriend").find(:all, :conditions => {:requestor_id => person.id, :target_id => friend.id}).blank?
  49 + end
  50 + end
  51 +
44 52 end
... ...
app/models/invite_member.rb
... ... @@ -2,6 +2,7 @@ class InviteMember &lt; Invitation
2 2  
3 3 settings_items :community_id, :type => :integer
4 4 validates_presence_of :community_id
  5 + before_create :check_for_invitation_existence
5 6  
6 7 def community
7 8 Community.find(community_id)
... ... @@ -39,6 +40,14 @@ class InviteMember &lt; Invitation
39 40 _('%{requestor} invited you to join %{community}.') % {:requestor => requestor.name, :community => community.name}
40 41 end
41 42  
  43 + def target_notification_message
  44 + if friend
  45 + _('%{requestor} is inviting you to join "%{community}" on %{system}.') % { :system => target.environment.name, :requestor => requestor.name, :community => community.name }
  46 + else
  47 + super
  48 + end
  49 + end
  50 +
42 51 def expanded_message
43 52 super.gsub /<community>/, community.name
44 53 end
... ... @@ -53,4 +62,11 @@ class InviteMember &lt; Invitation
53 62 ].join("\n\n")
54 63 end
55 64  
  65 + private
  66 + def check_for_invitation_existence
  67 + if friend
  68 + friend.tasks.pending.of("InviteMember").find(:all, :conditions => {:requestor_id => person.id}).select { |t| t.data[:community_id] == community_id }.blank?
  69 + end
  70 + end
  71 +
56 72 end
... ...
app/models/license.rb
... ... @@ -3,8 +3,8 @@ class License &lt; ActiveRecord::Base
3 3 attr_accessible :name, :url
4 4  
5 5 SEARCHABLE_FIELDS = {
6   - :name => 10,
7   - :url => 5,
  6 + :name => {:label => _('Name'), :weight => 10},
  7 + :url => {:label => _('URL'), :weight => 5},
8 8 }
9 9  
10 10 belongs_to :environment
... ...
app/models/link_article.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +class LinkArticle < Article
  2 +
  3 + attr_accessible :reference_article
  4 +
  5 + def self.short_description
  6 + "Article link"
  7 + end
  8 +
  9 + delegate :name, :to => :reference_article
  10 + delegate :body, :to => :reference_article
  11 + delegate :abstract, :to => :reference_article
  12 + delegate :url, :to => :reference_article
  13 +
  14 +end
... ...
app/models/national_region.rb
1 1 class NationalRegion < ActiveRecord::Base
2 2  
3 3 SEARCHABLE_FIELDS = {
4   - :name => 1,
5   - :national_region_code => 1,
  4 + :name => {:label => _('Name'), :weight => 1},
  5 + :national_region_code => {:label => _('Region Code'), :weight => 1},
6 6 }
7 7  
8 8 def self.search_city(city_name, like = false, state = nil)
... ...
app/models/organization.rb
... ... @@ -3,10 +3,11 @@ class Organization &lt; Profile
3 3  
4 4 attr_accessible :moderated_articles, :foundation_year, :contact_person, :acronym, :legal_form, :economic_activity, :management_information, :cnpj, :display_name, :enable_contact_us
5 5  
6   - SEARCH_FILTERS += %w[
7   - more_popular
8   - more_active
9   - ]
  6 + SEARCH_FILTERS = {
  7 + :order => %w[more_recent more_popular more_active],
  8 + :display => %w[compact]
  9 + }
  10 +
10 11  
11 12 settings_items :closed, :type => :boolean, :default => false
12 13 def closed?
... ... @@ -176,4 +177,8 @@ class Organization &lt; Profile
176 177 self.visible = false
177 178 save!
178 179 end
  180 +
  181 + def allow_invitation_from?(person)
  182 + (followed_by?(person) && self.allow_members_to_invite) || person.has_permission?('invite-members', self)
  183 + end
179 184 end
... ...
app/models/person.rb
... ... @@ -3,10 +3,11 @@ class Person &lt; Profile
3 3  
4 4 attr_accessible :organization, :contact_information, :sex, :birth_date, :cell_phone, :comercial_phone, :jabber_id, :personal_website, :nationality, :address_reference, :district, :schooling, :schooling_status, :formation, :custom_formation, :area_of_study, :custom_area_of_study, :professional_activity, :organization_website
5 5  
6   - SEARCH_FILTERS += %w[
7   - more_popular
8   - more_active
9   - ]
  6 + SEARCH_FILTERS = {
  7 + :order => %w[more_recent more_popular more_active],
  8 + :display => %w[compact]
  9 + }
  10 +
10 11  
11 12 def self.type_name
12 13 _('Person')
... ... @@ -21,16 +22,34 @@ class Person &lt; Profile
21 22 { :select => 'DISTINCT profiles.*', :joins => :role_assignments, :conditions => [conditions] }
22 23 }
23 24  
  25 + scope :not_members_of, lambda { |resources|
  26 + resources = [resources] if !resources.kind_of?(Array)
  27 + conditions = resources.map {|resource| "role_assignments.resource_type = '#{resource.class.base_class.name}' AND role_assignments.resource_id = #{resource.id || -1}"}.join(' OR ')
  28 + { :select => 'DISTINCT profiles.*', :conditions => ['"profiles"."id" NOT IN (SELECT DISTINCT profiles.id FROM "profiles" INNER JOIN "role_assignments" ON "role_assignments"."accessor_id" = "profiles"."id" AND "role_assignments"."accessor_type" = (\'Profile\') WHERE "profiles"."type" IN (\'Person\') AND (%s))' % conditions] }
  29 + }
  30 +
24 31 scope :by_role, lambda { |roles|
25 32 roles = [roles] unless roles.kind_of?(Array)
26 33 { :select => 'DISTINCT profiles.*', :joins => :role_assignments, :conditions => ['role_assignments.role_id IN (?)',
27 34 roles] }
28 35 }
29 36  
30   - def has_permission_with_plugins?(permission, profile)
31   - permissions = [has_permission_without_plugins?(permission, profile)]
  37 + scope :not_friends_of, lambda { |resources|
  38 + resources = Array(resources)
  39 + { :select => 'DISTINCT profiles.*', :conditions => ['"profiles"."id" NOT IN (SELECT DISTINCT profiles.id FROM "profiles" INNER JOIN "friendships" ON "friendships"."person_id" = "profiles"."id" WHERE "friendships"."friend_id" IN (%s))' % resources.map(&:id)] }
  40 + }
  41 +
  42 + def has_permission_with_admin?(permission, resource)
  43 + return true if resource.blank? || resource.admins.include?(self)
  44 + return true if resource.kind_of?(Profile) && resource.environment.admins.include?(self)
  45 + has_permission_without_admin?(permission, resource)
  46 + end
  47 + alias_method_chain :has_permission?, :admin
  48 +
  49 + def has_permission_with_plugins?(permission, resource)
  50 + permissions = [has_permission_without_plugins?(permission, resource)]
32 51 permissions += plugins.map do |plugin|
33   - plugin.has_permission?(self, permission, profile)
  52 + plugin.has_permission?(self, permission, resource)
34 53 end
35 54 permissions.include?(true)
36 55 end
... ... @@ -45,9 +64,9 @@ roles] }
45 64 ScopeTool.union *scopes
46 65 end
47 66  
48   - def memberships_by_role(role)
49   - memberships.where('role_assignments.role_id = ?', role.id)
50   - end
  67 + def memberships_by_role(role)
  68 + memberships.where('role_assignments.role_id = ?', role.id)
  69 + end
51 70  
52 71 has_many :friendships, :dependent => :destroy
53 72 has_many :friends, :class_name => 'Person', :through => :friendships
... ... @@ -65,6 +84,10 @@ roles] }
65 84 has_and_belongs_to_many :acepted_forums, :class_name => 'Forum', :join_table => 'terms_forum_people'
66 85 has_and_belongs_to_many :articles_with_access, :class_name => 'Article', :join_table => 'article_privacy_exceptions'
67 86  
  87 + has_many :profile_suggestions, :foreign_key => :person_id, :order => 'score DESC', :dependent => :destroy
  88 + has_many :suggested_people, :through => :profile_suggestions, :source => :suggestion, :conditions => ['profile_suggestions.suggestion_type = ? AND profile_suggestions.enabled = ?', 'Person', true]
  89 + has_many :suggested_communities, :through => :profile_suggestions, :source => :suggestion, :conditions => ['profile_suggestions.suggestion_type = ? AND profile_suggestions.enabled = ?', 'Community', true]
  90 +
68 91 scope :more_popular, :order => 'friends_count DESC'
69 92  
70 93 scope :abusers, :joins => :abuse_complaints, :conditions => ['tasks.status = 3'], :select => 'DISTINCT profiles.*'
... ... @@ -118,12 +141,12 @@ roles] }
118 141 end
119 142  
120 143 def add_friend(friend, group = nil)
121   - unless self.is_a_friend?(friend)
  144 + unless self.is_a_friend?(friend)
122 145 friendship = self.friendships.build
123 146 friendship.friend = friend
124 147 friendship.group = group
125 148 friendship.save
126   - end
  149 + end
127 150 end
128 151  
129 152 def already_request_friendship?(person)
... ... @@ -497,6 +520,15 @@ roles] }
497 520 person.notifier.reschedule_next_notification_mail
498 521 end
499 522  
  523 + def remove_suggestion(profile)
  524 + suggestion = profile_suggestions.find_by_suggestion_id profile.id
  525 + suggestion.disable if suggestion
  526 + end
  527 +
  528 + def allow_invitation_from?(person)
  529 + person.has_permission?(:manage_friends, self)
  530 + end
  531 +
500 532 protected
501 533  
502 534 def followed_by?(profile)
... ...
app/models/person_notifier.rb
... ... @@ -22,12 +22,17 @@ class PersonNotifier
22 22 schedule_next_notification_mail
23 23 end
24 24  
  25 + def notify_from
  26 + @person.last_notification || DateTime.now - @person.notification_time.hours
  27 + end
  28 +
25 29 def notify
26 30 if @person.notification_time && @person.notification_time > 0
27   - from = @person.last_notification || DateTime.now - @person.notification_time.hours
28   - notifications = @person.tracked_notifications.find(:all, :conditions => ["created_at > ?", from])
  31 + notifications = @person.tracked_notifications.find(:all, :conditions => ["created_at > ?", notify_from])
  32 + tasks = Task.to(@person).without_spam.pending.where("created_at > ?", notify_from).order_by('created_at', 'asc')
  33 +
29 34 Noosfero.with_locale @person.environment.default_language do
30   - Mailer::content_summary(@person, notifications).deliver unless notifications.empty?
  35 + Mailer::content_summary(@person, notifications, tasks).deliver unless notifications.empty? && tasks.empty?
31 36 end
32 37 @person.settings[:last_notification] = DateTime.now
33 38 @person.save!
... ... @@ -59,32 +64,42 @@ class PersonNotifier
59 64 end
60 65  
61 66 def failure(job)
62   - person = Person.find(person_id)
63   - person.notifier.dispatch_notification_mail
  67 + begin
  68 + person = Person.find(person_id)
  69 + person.notifier.dispatch_notification_mail
  70 + rescue
  71 + Rails.logger.error "PersonNotifier::NotifyJob: Cannot recover from failure"
  72 + end
64 73 end
65 74  
66 75 end
67 76  
68 77 class Mailer < ActionMailer::Base
69 78  
70   - add_template_helper(PersonNotifierHelper)
  79 + add_template_helper(ApplicationHelper)
71 80  
72 81 def session
73 82 {:theme => nil}
74 83 end
75 84  
76   - def content_summary(person, notifications)
  85 + def content_summary(person, notifications, tasks)
  86 + if person.environment
  87 + ActionMailer::Base.asset_host = person.environment.top_url
  88 + ActionMailer::Base.default_url_options[:host] = person.environment.default_hostname
  89 + end
  90 +
77 91 @current_theme = 'default'
78 92 @profile = person
79 93 @recipient = @profile.nickname || @profile.name
80 94 @notifications = notifications
  95 + @tasks = tasks
81 96 @environment = @profile.environment.name
82 97 @url = @profile.environment.top_url
83 98 mail(
84 99 content_type: "text/html",
85 100 from: "#{@profile.environment.name} <#{@profile.environment.noreply_email}>",
86 101 to: @profile.email,
87   - subject: _("[%s] Network Activity") % [@profile.environment.name]
  102 + subject: _("[%s] Notifications") % [@profile.environment.name]
88 103 )
89 104 end
90 105 end
... ...
app/models/product.rb
1 1 class Product < ActiveRecord::Base
2 2  
3 3 SEARCHABLE_FIELDS = {
4   - :name => 10,
5   - :description => 1,
  4 + :name => {:label => _('Name'), :weight => 10},
  5 + :description => {:label => _('Description'), :weight => 1},
6 6 }
7 7  
8   - SEARCH_FILTERS = %w[
9   - more_recent
10   - ]
11   -
12   - SEARCH_DISPLAYS = %w[map full]
  8 + SEARCH_FILTERS = {
  9 + :order => %w[more_recent],
  10 + :display => %w[full map]
  11 + }
13 12  
14   - attr_accessible :name, :product_category, :highlighted, :price, :enterprise, :image_builder, :description, :available, :qualifiers, :unit_id, :discount, :inputs, :qualifiers_list
  13 + attr_accessible :name, :product_category, :profile, :profile_id, :enterprise,
  14 + :highlighted, :price, :image_builder, :description, :available, :qualifiers, :unit_id, :discount, :inputs, :qualifiers_list
15 15  
16 16 def self.default_search_display
17 17 'full'
... ... @@ -237,7 +237,7 @@ class Product &lt; ActiveRecord::Base
237 237  
238 238 def percentage_from_solidarity_economy
239 239 se_i = t_i = 0
240   - self.inputs(true).each{ |i| t_i += 1; se_i += 1 if i.is_from_solidarity_economy }
  240 + self.inputs.each{ |i| t_i += 1; se_i += 1 if i.is_from_solidarity_economy }
241 241 t_i = 1 if t_i == 0 # avoid division by 0
242 242 p = case (se_i.to_f/t_i)*100
243 243 when 0 then [0, '']
... ...
app/models/profile.rb
... ... @@ -3,7 +3,7 @@
3 3 # which by default is the one returned by Environment:default.
4 4 class Profile < ActiveRecord::Base
5 5  
6   - attr_accessible :name, :identifier, :public_profile, :nickname, :custom_footer, :custom_header, :address, :zip_code, :contact_phone, :image_builder, :description, :closed, :template_id, :environment, :lat, :lng, :is_template, :fields_privacy, :preferred_domain_id, :category_ids, :country, :city, :state, :national_region_code, :email, :contact_email, :redirect_l10n, :notification_time, :redirection_after_login
  6 + attr_accessible :name, :identifier, :public_profile, :nickname, :custom_footer, :custom_header, :address, :zip_code, :contact_phone, :image_builder, :description, :closed, :template_id, :environment, :lat, :lng, :is_template, :fields_privacy, :preferred_domain_id, :category_ids, :country, :city, :state, :national_region_code, :email, :contact_email, :redirect_l10n, :notification_time, :redirection_after_login, :email_suggestions, :allow_members_to_invite, :invite_friends_only
7 7  
8 8 # use for internationalizable human type names in search facets
9 9 # reimplement on subclasses
... ... @@ -12,16 +12,15 @@ class Profile &lt; ActiveRecord::Base
12 12 end
13 13  
14 14 SEARCHABLE_FIELDS = {
15   - :name => 10,
16   - :identifier => 5,
17   - :nickname => 2,
  15 + :name => {:label => _('Name'), :weight => 10},
  16 + :identifier => {:label => _('Username'), :weight => 5},
  17 + :nickname => {:label => _('Nickname'), :weight => 2},
18 18 }
19 19  
20   - SEARCH_FILTERS = %w[
21   - more_recent
22   - ]
23   -
24   - SEARCH_DISPLAYS = %w[compact]
  20 + SEARCH_FILTERS = {
  21 + :order => %w[more_recent],
  22 + :display => %w[compact]
  23 + }
25 24  
26 25 def self.default_search_display
27 26 'compact'
... ... @@ -123,6 +122,7 @@ class Profile &lt; ActiveRecord::Base
123 122 scope :visible, :conditions => { :visible => true }
124 123 scope :disabled, :conditions => { :visible => false }
125 124 scope :public, :conditions => { :visible => true, :public_profile => true }
  125 + scope :enabled, :conditions => { :enabled => true }
126 126  
127 127 # Subclasses must override this method
128 128 scope :more_popular
... ... @@ -139,6 +139,17 @@ class Profile &lt; ActiveRecord::Base
139 139  
140 140 has_many :comments_received, :class_name => 'Comment', :through => :articles, :source => :comments
141 141  
  142 + # Although this should be a has_one relation, there are no non-silly names for
  143 + # a foreign key on article to reference the template to which it is
  144 + # welcome_page... =P
  145 + belongs_to :welcome_page, :class_name => 'Article', :dependent => :destroy
  146 +
  147 + def welcome_page_content
  148 + welcome_page && welcome_page.published ? welcome_page.body : nil
  149 + end
  150 +
  151 + has_many :search_terms, :as => :context
  152 +
142 153 def scraps(scrap=nil)
143 154 scrap = scrap.is_a?(Scrap) ? scrap.id : scrap
144 155 scrap.nil? ? Scrap.all_scraps(self) : Scrap.all_scraps(self).find(scrap)
... ... @@ -154,6 +165,7 @@ class Profile &lt; ActiveRecord::Base
154 165 settings_items :public_content, :type => :boolean, :default => true
155 166 settings_items :description
156 167 settings_items :fields_privacy, :type => :hash, :default => {}
  168 + settings_items :email_suggestions, :type => :boolean, :default => false
157 169  
158 170 validates_length_of :description, :maximum => 550, :allow_nil => true
159 171  
... ... @@ -218,6 +230,8 @@ class Profile &lt; ActiveRecord::Base
218 230  
219 231 has_many :abuse_complaints, :foreign_key => 'requestor_id', :dependent => :destroy
220 232  
  233 + has_many :profile_suggestions, :foreign_key => :suggestion_id, :dependent => :destroy
  234 +
221 235 def top_level_categorization
222 236 ret = {}
223 237 self.profile_categorizations.each do |c|
... ... @@ -392,7 +406,7 @@ class Profile &lt; ActiveRecord::Base
392 406 end
393 407  
394 408 xss_terminate :only => [ :name, :nickname, :address, :contact_phone, :description ], :on => 'validation'
395   - xss_terminate :only => [ :custom_footer, :custom_header ], :with => 'white_list', :on => 'validation'
  409 + xss_terminate :only => [ :custom_footer, :custom_header ], :with => 'white_list'
396 410  
397 411 include WhiteListFilter
398 412 filter_iframes :custom_header, :custom_footer
... ... @@ -513,6 +527,14 @@ class Profile &lt; ActiveRecord::Base
513 527 generate_url(:profile => identifier, :controller => 'profile', :action => 'index')
514 528 end
515 529  
  530 + def people_suggestions_url
  531 + generate_url(:profile => identifier, :controller => 'friends', :action => 'suggest')
  532 + end
  533 +
  534 + def communities_suggestions_url
  535 + generate_url(:profile => identifier, :controller => 'memberships', :action => 'suggest')
  536 + end
  537 +
516 538 def generate_url(options)
517 539 url_options.merge(options)
518 540 end
... ... @@ -602,7 +624,7 @@ private :generate_url, :url_options
602 624 end
603 625  
604 626 def copy_article_tree(article, parent=nil)
605   - return if article.is_a?(RssFeed)
  627 + return if !copy_article?(article)
606 628 original_article = self.articles.find_by_name(article.name)
607 629 if original_article
608 630 num = 2
... ... @@ -622,6 +644,11 @@ private :generate_url, :url_options
622 644 end
623 645 end
624 646  
  647 + def copy_article?(article)
  648 + !article.is_a?(RssFeed) &&
  649 + !(is_template && article.slug=='welcome-page')
  650 + end
  651 +
625 652 # Adds a person as member of this Profile.
626 653 def add_member(person)
627 654 if self.has_members?
... ... @@ -631,6 +658,8 @@ private :generate_url, :url_options
631 658 self.affiliate(person, Profile::Roles.admin(environment.id)) if members.count == 0
632 659 self.affiliate(person, Profile::Roles.member(environment.id))
633 660 end
  661 + person.tasks.pending.of("InviteMember").select { |t| t.data[:community_id] == self.id }.each { |invite| invite.cancel }
  662 + remove_from_suggestion_list person
634 663 else
635 664 raise _("%s can't have members") % self.class.name
636 665 end
... ... @@ -768,7 +797,10 @@ private :generate_url, :url_options
768 797 end
769 798  
770 799 def admins
771   - self.members_by_role(Profile::Roles.admin(environment.id))
  800 + return [] if environment.blank?
  801 + admin_role = Profile::Roles.admin(environment.id)
  802 + return [] if admin_role.blank?
  803 + self.members_by_role(admin_role)
772 804 end
773 805  
774 806 def enable_contact?
... ... @@ -966,4 +998,14 @@ private :generate_url, :url_options
966 998 def preferred_login_redirection
967 999 redirection_after_login.blank? ? environment.redirection_after_login : redirection_after_login
968 1000 end
  1001 +
  1002 + def remove_from_suggestion_list(person)
  1003 + suggestion = person.profile_suggestions.find_by_suggestion_id self.id
  1004 + suggestion.disable if suggestion
  1005 + end
  1006 +
  1007 + def allow_invitation_from(person)
  1008 + false
  1009 + end
  1010 +
969 1011 end
... ...
app/models/profile_suggestion.rb 0 → 100644
... ... @@ -0,0 +1,290 @@
  1 +class ProfileSuggestion < ActiveRecord::Base
  2 + belongs_to :person
  3 + belongs_to :suggestion, :class_name => 'Profile', :foreign_key => :suggestion_id
  4 +
  5 + attr_accessible :person, :suggestion, :suggestion_type, :categories, :enabled
  6 +
  7 + has_many :suggestion_connections, :foreign_key => 'suggestion_id'
  8 + has_many :profile_connections, :through => :suggestion_connections, :source => :connection, :source_type => 'Profile'
  9 + has_many :tag_connections, :through => :suggestion_connections, :source => :connection, :source_type => 'ActsAsTaggableOn::Tag'
  10 +
  11 + before_create do |profile_suggestion|
  12 + profile_suggestion.suggestion_type = self.suggestion.class.to_s
  13 + end
  14 +
  15 + after_destroy do |profile_suggestion|
  16 + self.class.generate_profile_suggestions(profile_suggestion.person)
  17 + end
  18 +
  19 + acts_as_having_settings :field => :categories
  20 +
  21 + validate :must_be_a_valid_category, :on => :create
  22 + def must_be_a_valid_category
  23 + if categories.keys.map { |cat| self.respond_to?(cat)}.include?(false)
  24 + errors.add(:categories, 'Category must be valid')
  25 + end
  26 + end
  27 +
  28 + validates_uniqueness_of :suggestion_id, :scope => [ :person_id ]
  29 + scope :of_person, :conditions => { :suggestion_type => 'Person' }
  30 + scope :of_community, :conditions => { :suggestion_type => 'Community' }
  31 + scope :enabled, :conditions => { :enabled => true }
  32 +
  33 + # {:category_type => ['category-icon', 'category-label']}
  34 + CATEGORIES = {
  35 + :people_with_common_friends => ['menu-people', _('Friends in common')],
  36 + :people_with_common_communities => ['menu-community',_('Communities in common')],
  37 + :people_with_common_tags => ['edit', _('Tags in common')],
  38 + :communities_with_common_friends => ['menu-people', _('Friends in common')],
  39 + :communities_with_common_tags => ['edit', _('Tags in common')]
  40 + }
  41 +
  42 + def category_icon(category)
  43 + 'icon-' + ProfileSuggestion::CATEGORIES[category][0]
  44 + end
  45 +
  46 + def category_label(category)
  47 + ProfileSuggestion::CATEGORIES[category][1]
  48 + end
  49 +
  50 + RULES = {
  51 + :people_with_common_communities => {
  52 + :threshold => 2, :weight => 1, :connection => 'Profile'
  53 + },
  54 + :people_with_common_friends => {
  55 + :threshold => 2, :weight => 1, :connection => 'Profile'
  56 + },
  57 + :people_with_common_tags => {
  58 + :threshold => 2, :weight => 1, :connection => 'ActsAsTaggableOn::Tag'
  59 + },
  60 + :communities_with_common_friends => {
  61 + :threshold => 2, :weight => 1, :connection => 'Profile'
  62 + },
  63 + :communities_with_common_tags => {
  64 + :threshold => 2, :weight => 1, :connection => 'ActsAsTaggableOn::Tag'
  65 + }
  66 + }
  67 +
  68 + RULES.keys.each do |rule|
  69 + settings_items rule
  70 + attr_accessible rule
  71 + end
  72 +
  73 + # Number of suggestions by rule
  74 + N_SUGGESTIONS = 30
  75 +
  76 + # Minimum number of suggestions
  77 + MIN_LIMIT = 10
  78 +
  79 + def self.profile_id(rule)
  80 + "#{rule}_profile_id"
  81 + end
  82 +
  83 + def self.connections(rule)
  84 + "#{rule}_connections"
  85 + end
  86 +
  87 + def self.counter(rule)
  88 + "#{rule}_count"
  89 + end
  90 +
  91 + # If you are about to rewrite the following sql queries, think twice. After
  92 + # that make sure that whatever you are writing to replace it should be faster
  93 + # than how it is now. Yes, sqls are ugly but are fast! And fast is what we
  94 + # need here.
  95 + #
  96 + # The logic behind this code is to produce a table somewhat like this:
  97 + # profile_id | rule1_count | rule1_connections | rule2_count | rule2_connections | ... | score |
  98 + # 12 | 2 | {32,54} | 3 | {8,22,27} | ... | 13 |
  99 + # 13 | 4 | {3,12,32,54} | 2 | {11,24} | ... | 15 |
  100 + # 14 | | | 2 | {44,56} | ... | 17 |
  101 + # ...
  102 + # ...
  103 + #
  104 + # This table has the suggested profile id and the count and connections of
  105 + # each rule that made this profile be suggested. Each suggestion has a score
  106 + # associated based on the rules' counts and rules' weights.
  107 + #
  108 + # From this table, we can sort suggestions by the score and save a small
  109 + # amount of them in the database. At this moment we also register the
  110 + # connections of each suggestion.
  111 +
  112 + def self.calculate_suggestions(person)
  113 + suggested_profiles = all_suggestions(person)
  114 + return if suggested_profiles.nil?
  115 +
  116 + already_suggested_profiles = person.profile_suggestions.map(&:suggestion_id).join(',')
  117 + suggested_profiles = suggested_profiles.where("profiles.id NOT IN (#{already_suggested_profiles})") if already_suggested_profiles.present?
  118 + #TODO suggested_profiles = suggested_profiles.order('score DESC')
  119 + suggested_profiles = suggested_profiles.limit(N_SUGGESTIONS)
  120 + return if suggested_profiles.blank?
  121 +
  122 + suggested_profiles.each do |suggested_profile|
  123 + suggestion = person.profile_suggestions.find_or_initialize_by_suggestion_id(suggested_profile.id)
  124 + RULES.each do |rule, options|
  125 + begin
  126 + value = suggested_profile.send("#{rule}_count").to_i
  127 + rescue NoMethodError
  128 + next
  129 + end
  130 + connections = suggested_profile.send("#{rule}_connections")
  131 + if connections.present?
  132 + connections = connections[1..-2].split(',')
  133 + else
  134 + connections = []
  135 + end
  136 + suggestion.send("#{rule}=", value)
  137 + connections.each do |connection_id|
  138 + next if SuggestionConnection.where(:suggestion_id => suggestion.id, :connection_id => connection_id, :connection_type => options[:connection]).present?
  139 + SuggestionConnection.create!(:suggestion => suggestion, :connection_id => connection_id, :connection_type => options[:connection])
  140 + end
  141 + suggestion.score += value * options[:weight]
  142 + end
  143 + suggestion.save!
  144 + end
  145 + end
  146 +
  147 + def self.people_with_common_friends(person)
  148 + person_friends = person.friends.map(&:id)
  149 + rule = "people_with_common_friends"
  150 + return if person_friends.blank?
  151 + "SELECT person_id as #{profile_id(rule)},
  152 + array_agg(friend_id) as #{connections(rule)},
  153 + count(person_id) as #{counter(rule)}
  154 + FROM friendships WHERE friend_id IN (#{person_friends.join(',')})
  155 + AND person_id NOT IN (#{(person_friends << person.id).join(',')})
  156 + GROUP BY person_id"
  157 + end
  158 +
  159 + def self.people_with_common_communities(person)
  160 + person_communities = person.communities.map(&:id)
  161 + rule = "people_with_common_communities"
  162 + return if person_communities.blank?
  163 + "SELECT common_members.accessor_id as #{profile_id(rule)},
  164 + array_agg(common_members.resource_id) as #{connections(rule)},
  165 + count(common_members.accessor_id) as #{counter(rule)}
  166 + FROM
  167 + (SELECT DISTINCT accessor_id, resource_id FROM
  168 + role_assignments WHERE role_assignments.resource_id IN (#{person_communities.join(',')}) AND
  169 + role_assignments.accessor_id != #{person.id} AND role_assignments.resource_type = 'Profile' AND
  170 + role_assignments.accessor_type = 'Profile') AS common_members
  171 + GROUP BY common_members.accessor_id"
  172 + end
  173 +
  174 + def self.people_with_common_tags(person)
  175 + profile_tags = person.articles.select('tags.id').joins(:tags).map(&:id)
  176 + rule = "people_with_common_tags"
  177 + return if profile_tags.blank?
  178 + "SELECT results.profiles_id as #{profile_id(rule)},
  179 + array_agg(results.tags_id) as #{connections(rule)},
  180 + count(results.profiles_id) as #{counter(rule)}
  181 + FROM (
  182 + SELECT DISTINCT tags.id as tags_id, profiles.id as profiles_id FROM profiles
  183 + INNER JOIN articles ON articles.profile_id = profiles.id
  184 + INNER JOIN taggings ON taggings.taggable_id = articles.id AND taggings.context = ('tags') AND taggings.taggable_type = 'Article'
  185 + INNER JOIN tags ON tags.id = taggings.tag_id
  186 + WHERE (tags.id in (#{profile_tags.join(',')}) AND profiles.id != #{person.id})) AS results
  187 + GROUP BY results.profiles_id"
  188 + end
  189 +
  190 + def self.communities_with_common_friends(person)
  191 + person_friends = person.friends.map(&:id)
  192 + rule = "communities_with_common_friends"
  193 + return if person_friends.blank?
  194 + "SELECT common_communities.resource_id as #{profile_id(rule)},
  195 + array_agg(common_communities.accessor_id) as #{connections(rule)},
  196 + count(common_communities.resource_id) as #{counter(rule)}
  197 + FROM
  198 + (SELECT DISTINCT accessor_id, resource_id FROM
  199 + role_assignments WHERE role_assignments.accessor_id IN (#{person_friends.join(',')}) AND
  200 + role_assignments.accessor_id != #{person.id} AND role_assignments.resource_type = 'Profile' AND
  201 + role_assignments.accessor_type = 'Profile') AS common_communities
  202 + GROUP BY common_communities.resource_id"
  203 + end
  204 +
  205 + def self.communities_with_common_tags(person)
  206 + profile_tags = person.articles.select('tags.id').joins(:tags).map(&:id)
  207 + rule = "communities_with_common_tags"
  208 + return if profile_tags.blank?
  209 + "SELECT results.profiles_id as #{profile_id(rule)},
  210 + array_agg(results.tags_id) as #{connections(rule)},
  211 + count(results.profiles_id) as #{counter(rule)}
  212 + FROM
  213 + (SELECT DISTINCT tags.id as tags_id, profiles.id AS profiles_id FROM profiles
  214 + INNER JOIN articles ON articles.profile_id = profiles.id
  215 + INNER JOIN taggings ON taggings.taggable_id = articles.id AND taggings.context = ('tags') AND taggings.taggable_type = 'Article'
  216 + INNER JOIN tags ON tags.id = taggings.tag_id
  217 + WHERE (tags.id IN (#{profile_tags.join(',')}) AND profiles.id != #{person.id})) AS results
  218 + GROUP BY results.profiles_id"
  219 + end
  220 +
  221 + def self.all_suggestions(person)
  222 + select_string = ["profiles.*"]
  223 + suggestions_join = []
  224 + where_string = []
  225 + valid_rules = []
  226 + previous_rule = nil
  227 + join_column = nil
  228 + RULES.each do |rule, options|
  229 + rule_select = self.send(rule, person)
  230 + next if !rule_select.present?
  231 +
  232 + valid_rules << rule
  233 + select_string << "suggestions.#{counter(rule)} as #{counter(rule)}, suggestions.#{connections(rule)} as #{connections(rule)}"
  234 + where_string << "#{counter(rule)} >= #{options[:threshold]}"
  235 + rule_select = "
  236 + (SELECT profiles.id as #{profile_id(rule)},
  237 + #{rule}_sub.#{counter(rule)} as #{counter(rule)},
  238 + #{rule}_sub.#{connections(rule)} as #{connections(rule)}
  239 + FROM profiles
  240 + LEFT OUTER JOIN (#{rule_select}) as #{rule}_sub
  241 + ON profiles.id = #{rule}_sub.#{profile_id(rule)}) AS #{rule}"
  242 +
  243 + if previous_rule.nil?
  244 + result = rule_select
  245 + else
  246 + result = "INNER JOIN #{rule_select}
  247 + ON #{previous_rule}.#{profile_id(previous_rule)} = #{rule}.#{profile_id(rule)}"
  248 + end
  249 + previous_rule = rule
  250 + suggestions_join << result
  251 + end
  252 +
  253 + return if valid_rules.blank?
  254 +
  255 + select_string = select_string.compact.join(',')
  256 + join_string = "INNER JOIN (SELECT * FROM #{suggestions_join.compact.join(' ')}) AS suggestions ON profiles.id = suggestions.#{profile_id(valid_rules.first)}"
  257 + where_string = where_string.compact.join(' OR ')
  258 +
  259 + person.environment.profiles.
  260 + select(select_string).
  261 + joins(join_string).
  262 + where(where_string)
  263 + end
  264 +
  265 + def disable
  266 + self.enabled = false
  267 + self.save!
  268 + self.class.generate_profile_suggestions(self.person)
  269 + end
  270 +
  271 + def self.generate_all_profile_suggestions
  272 + Delayed::Job.enqueue(ProfileSuggestion::GenerateAllJob.new) unless ProfileSuggestion::GenerateAllJob.exists?
  273 + end
  274 +
  275 + def self.generate_profile_suggestions(person, force = false)
  276 + return if person.profile_suggestions.enabled.count >= MIN_LIMIT && !force
  277 + Delayed::Job.enqueue ProfileSuggestionsJob.new(person.id) unless ProfileSuggestionsJob.exists?(person.id)
  278 + end
  279 +
  280 + class GenerateAllJob
  281 + def self.exists?
  282 + Delayed::Job.by_handler("--- !ruby/object:ProfileSuggestion::GenerateAllJob {}\n").count > 0
  283 + end
  284 +
  285 + def perform
  286 + Person.find_each {|person| ProfileSuggestion.generate_profile_suggestions(person) }
  287 + end
  288 + end
  289 +
  290 +end
... ...
app/models/qualifier.rb
... ... @@ -3,7 +3,7 @@ class Qualifier &lt; ActiveRecord::Base
3 3 attr_accessible :name, :environment
4 4  
5 5 SEARCHABLE_FIELDS = {
6   - :name => 1,
  6 + :name => {:label => _('Name'), :weight => 1},
7 7 }
8 8  
9 9 belongs_to :environment
... ...
app/models/scrap.rb
... ... @@ -3,7 +3,7 @@ class Scrap &lt; ActiveRecord::Base
3 3 attr_accessible :content, :sender_id, :receiver_id, :scrap_id
4 4  
5 5 SEARCHABLE_FIELDS = {
6   - :content => 1,
  6 + :content => {:label => _('Content'), :weight => 1},
7 7 }
8 8 validates_presence_of :content
9 9 validates_presence_of :sender_id, :receiver_id
... ... @@ -25,7 +25,7 @@ class Scrap &lt; ActiveRecord::Base
25 25  
26 26 after_create do |scrap|
27 27 scrap.root.update_attribute('updated_at', DateTime.now) unless scrap.root.nil?
28   - Scrap::Notifier.notification(scrap).deliver if scrap.send_notification?
  28 + ScrapNotifier.notification(scrap).deliver if scrap.send_notification?
29 29 end
30 30  
31 31 before_validation :strip_all_html_tags
... ...
app/models/search_term.rb 0 → 100644
... ... @@ -0,0 +1,63 @@
  1 +class SearchTerm < ActiveRecord::Base
  2 + validates_presence_of :term, :context
  3 + validates_uniqueness_of :term, :scope => [:context_id, :context_type, :asset]
  4 +
  5 + belongs_to :context, :polymorphic => true
  6 + has_many :occurrences, :class_name => 'SearchTermOccurrence'
  7 +
  8 + attr_accessible :term, :context, :asset
  9 +
  10 + def self.calculate_scores
  11 + os = occurrences_scores
  12 + find_each { |search_term| search_term.calculate_score(os) }
  13 + end
  14 +
  15 + def self.find_or_create(term, context, asset='all')
  16 + context.search_terms.where(:term => term, :asset => asset).first || context.search_terms.create!(:term => term, :asset=> asset)
  17 + end
  18 +
  19 + # Fast way of getting the occurrences score for each search_term. Ugly but fast!
  20 + #
  21 + # Each occurrence of a search_term has a score that is smaller the older the
  22 + # occurrence happened. We subtract the amount of time between now and the
  23 + # moment it happened from the total time any occurrence is valid to happen. E.g.:
  24 + # The expiration time is 100 days and an occurrence happened 3 days ago.
  25 + # Therefore the score is 97. Them we sum every score to get the total score
  26 + # for a search term.
  27 + def self.occurrences_scores
  28 + ActiveSupport::OrderedHash[*ActiveRecord::Base.connection.execute(
  29 + joins(:occurrences).
  30 + select("search_terms.id, sum(#{SearchTermOccurrence::EXPIRATION_TIME.to_i} - extract(epoch from (now() - search_term_occurrences.created_at))) as value").
  31 + where("search_term_occurrences.created_at > ?", DateTime.now - SearchTermOccurrence::EXPIRATION_TIME).
  32 + group("search_terms.id").
  33 + order('value DESC').
  34 + to_sql
  35 + ).map {|result| [result['id'].to_i, result['value'].to_i]}.flatten]
  36 + end
  37 +
  38 + def calculate_occurrence(occurrences_scores)
  39 + max_score = occurrences_scores.first[1]
  40 + (occurrences_scores[id]/max_score.to_f)*100
  41 + end
  42 +
  43 + def calculate_relevance(valid_occurrences)
  44 + indexed = valid_occurrences.last.indexed.to_f
  45 + return 0 if indexed == 0
  46 + total = valid_occurrences.last.total.to_f
  47 + (1 - indexed/total)*100
  48 + end
  49 +
  50 + def calculate_score(occurrences_scores)
  51 + valid_occurrences = occurrences.valid
  52 + if valid_occurrences.present?
  53 + # These scores vary from 1~100
  54 + self.occurrence_score = calculate_occurrence(occurrences_scores)
  55 + self.relevance_score = calculate_relevance(valid_occurrences)
  56 + else
  57 + self.occurrence_score = 0
  58 + self.relevance_score = 0
  59 + end
  60 + self.score = (occurrence_score * relevance_score)/100.0
  61 + self.save!
  62 + end
  63 +end
... ...
app/models/search_term_occurrence.rb 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +class SearchTermOccurrence < ActiveRecord::Base
  2 + belongs_to :search_term
  3 + validates_presence_of :search_term
  4 + attr_accessible :search_term, :created_at, :total, :indexed
  5 +
  6 + EXPIRATION_TIME = 1.year
  7 +
  8 + scope :valid, :conditions => ["search_term_occurrences.created_at > ?", DateTime.now - EXPIRATION_TIME]
  9 +end
... ...
app/models/suggestion_connection.rb 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +class SuggestionConnection < ActiveRecord::Base
  2 + attr_accessible :suggestion, :connection_type, :connection_id
  3 +
  4 + belongs_to :suggestion, :class_name => 'ProfileSuggestion', :foreign_key => 'suggestion_id'
  5 + belongs_to :connection, :polymorphic => true
  6 +end
... ...
app/models/text_article.rb
1 1 require 'noosfero/translatable_content'
2 2  
3   -# a base class for all text article types.
  3 +# a base class for all text article types.
4 4 class TextArticle < Article
5 5  
6 6 xss_terminate :only => [ :name ], :on => 'validation'
... ... @@ -26,10 +26,10 @@ class TextArticle &lt; Article
26 26 before_save :set_relative_path
27 27  
28 28 def set_relative_path
29   - parsed = Hpricot(self.body.to_s)
30   - parsed.search('img[@src]').map { |i| change_element_path(i, 'src') }
31   - parsed.search('a[@href]').map { |i| change_element_path(i, 'href') }
32   - self.body = parsed.to_s
  29 + parsed = Nokogiri::HTML.fragment(self.body.to_s)
  30 + parsed.css('img[src]').each { |i| change_element_path(i, 'src') }
  31 + parsed.css('a[href]').each { |i| change_element_path(i, 'href') }
  32 + self.body = parsed.to_html
33 33 end
34 34  
35 35 def change_element_path(el, attribute)
... ...
app/models/user.rb
... ... @@ -11,6 +11,10 @@ class User &lt; ActiveRecord::Base
11 11 N_('Password confirmation')
12 12 N_('Terms accepted')
13 13  
  14 + SEARCHABLE_FIELDS = {
  15 + :email => {:label => _('Email'), :weight => 5},
  16 + }
  17 +
14 18 def self.[](login)
15 19 self.find_by_login(login)
16 20 end
... ... @@ -50,7 +54,7 @@ class User &lt; ActiveRecord::Base
50 54  
51 55 user.person = p
52 56 end
53   - if user.environment.enabled?('skip_new_user_email_confirmation')
  57 + if user.environment.enabled?('skip_new_user_email_confirmation')
54 58 if user.environment.enabled?('admin_must_approve_new_users')
55 59 create_moderate_task
56 60 else
... ... @@ -98,6 +102,7 @@ class User &lt; ActiveRecord::Base
98 102 validates_length_of :email, :within => 3..100, :if => (lambda {|user| !user.email.blank?})
99 103 validates_uniqueness_of :login, :email, :case_sensitive => false, :scope => :environment_id
100 104 before_save :encrypt_password
  105 + before_save :normalize_email, if: proc{ |u| u.email.present? }
101 106 validates_format_of :email, :with => Noosfero::Constants::EMAIL_FORMAT, :if => (lambda {|user| !user.email.blank?})
102 107  
103 108 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
... ... @@ -110,6 +115,10 @@ class User &lt; ActiveRecord::Base
110 115 u && u.authenticated?(password) ? u : nil
111 116 end
112 117  
  118 + def register_login
  119 + self.update_attribute :last_login_at, Time.now
  120 + end
  121 +
113 122 # Activates the user in the database.
114 123 def activate
115 124 return false unless self.person
... ... @@ -328,6 +337,11 @@ class User &lt; ActiveRecord::Base
328 337 end
329 338  
330 339 protected
  340 +
  341 + def normalize_email
  342 + self.email = self.email.squish.downcase
  343 + end
  344 +
331 345 # before filter
332 346 def encrypt_password
333 347 return if password.blank?
... ... @@ -355,6 +369,6 @@ class User &lt; ActiveRecord::Base
355 369  
356 370 def delay_activation_check
357 371 return if person.is_template?
358   - Delayed::Job.enqueue(UserActivationJob.new(self.id), {:priority => 0, :run_at => 72.hours.from_now})
  372 + Delayed::Job.enqueue(UserActivationJob.new(self.id), {:priority => 0, :run_at => (NOOSFERO_CONF['hours_until_user_activation_check'] || 72).hours.from_now})
359 373 end
360 374 end
... ...
app/views/account/_login_form.html.erb
1 1 <%= labelled_form_for :user,
2 2 :url => { :controller => 'account', :action => (params[:enterprise_code] ? 'activate_enterprise' : 'login') } do |f| %>
3 3  
4   -<%= f.text_field :login,
5   - :id => ( lightbox? ? 'lightbox_' : '' ) + 'user_login',
6   - :onchange => 'this.value = convToValidLogin( this.value )' %>
  4 +<%= f.text_field :login, :id => 'user_login', :onchange => 'this.value = convToValidLogin( this.value )' %>
7 5  
8   -<%= f.password_field :password,
9   - :id => ( lightbox? ? 'lightbox_' : '' ) + 'user_password' %>
  6 +<%= f.password_field :password, :id => 'user_password' %>
10 7  
11 8 <% if params[:enterprise_code] %>
12 9 <%= hidden_field_tag :enterprise_code, params[:enterprise_code] %>
... ... @@ -16,7 +13,7 @@
16 13  
17 14 <% button_bar do %>
18 15 <%= submit_button( 'login', _('Log in') )%>
19   - <%= lightbox_close_button(_('Cancel')) if lightbox? %>
  16 + <%= modal_close_button _('Cancel') if request.xhr? %>
20 17 <% end %>
21 18  
22 19 <% end %>
... ...
app/views/account/_signup_form.html.erb
... ... @@ -16,7 +16,7 @@
16 16 <input type="hidden" id="signup_time_key" name="signup_time_key" />
17 17 <script type="text/javascript">
18 18 jQuery.ajax({
19   - type: "POST",
  19 + type: "GET",
20 20 url: "<%= url_for :controller=>'account', :action=>'signup_time' %>",
21 21 dataType: 'json',
22 22 success: function(data) {
... ...
app/views/account/activate_enterprise.html.erb
... ... @@ -7,8 +7,8 @@
7 7 <p><%= _('Do you have a personal user account in the system?') %></p>
8 8  
9 9 <div id="enterprise-activation-create-user-or-login-button">
10   - <%= button_to_function 'login', _('Yes'), "$('enterprise-activation-create-user-form').hide(); $('enterprise-activation-login-form').show()" %>
11   - <%= button_to_function 'add', _('No'), "$('enterprise-activation-login-form').hide(); $('enterprise-activation-create-user-form').show()" %>
  10 + <%= button_to_function 'login', _('Yes'), "jQuery('#enterprise-activation-create-user-form').hide(); jQuery('#enterprise-activation-login-form').show()" %>
  11 + <%= button_to_function 'add', _('No'), "jQuery('#enterprise-activation-login-form').hide(); jQuery('#enterprise-activation-create-user-form').show()" %>
12 12 </div>
13 13  
14 14 <div id="enterprise-activation-create-user-form" style="display: none">
... ...
app/views/account/index_anonymous.html.erb
1 1 <h1><%= _('Identify yourself') %></h1>
2 2  
3 3 <p>
4   -<%= lightbox_link_to _('Login.'), { :controller => 'account', :action => 'login_popup' } %>
  4 +<%= modal_link_to _('Login.'), { :controller => 'account', :action => 'login_popup' } %>
5 5  
6 6 <%= _('You need to login to be able to use all the features in this environment.') %>
7 7 </p>
... ...
app/views/account/login.html.erb
... ... @@ -3,11 +3,11 @@
3 3 <h2><%= _('Login') %></h2>
4 4  
5 5 <% @user ||= User.new %>
6   -<% is_thickbox ||= false %>
  6 +<% is_popin ||= false %>
7 7  
8 8 <%= @message %>
9 9  
10   -<%= labelled_form_for :user, :url => login_url do |f| %>
  10 +<%= labelled_form_for :user, :url => login_url, :horizontal => true do |f| %>
11 11  
12 12 <%= f.text_field :login, :id => 'main_user_login', :onchange => 'this.value = convToValidLogin( this.value )', :value => params[:userlogin] %>
13 13  
... ... @@ -17,8 +17,8 @@
17 17  
18 18 <% button_bar do %>
19 19 <%= submit_button( 'login', _('Log in') )%>
20   - <% if is_thickbox %>
21   - <%= thickbox_close_button(_('Cancel')) %>
  20 + <% if is_popin %>
  21 + <%= modal_close_button(_('Cancel')) %>
22 22 <% end %>
23 23 <% end %>
24 24  
... ...
app/views/account/login_block.html.erb
... ... @@ -20,9 +20,7 @@
20 20 <% button_bar do %>
21 21 <%= submit_button( 'login', _('Log in') )%>
22 22 <% unless @plugins.dispatch(:allow_user_registration).include?(false) %>
23   - <%= link_to content_tag( 'span', _('New user') ),
24   - { :controller => 'account', :action => 'signup' },
25   - :class => 'button with-text icon-add' %>
  23 + <%= button(:add, _('New user'), { :controller => 'account', :action => 'signup' }) %>
26 24 <% end %>
27 25 <% end %>
28 26  
... ...
app/views/account/logout_popup.html.erb
... ... @@ -2,6 +2,6 @@
2 2 <p>
3 3 <% button_bar do %>
4 4 <%= button :ok, _('Yes'), { :controller => 'account', :action => 'logout' } %>
5   - <%= lightbox_close_button _('No, I want to stay.') %>
  5 + <%= modal_close_button _('No, I want to stay.') %>
6 6 <% end %>
7 7 </p>
... ...
app/views/account/signup.html.erb
1   -<% if @register_pending %>
2   - <div id='thanks-for-signing'>
3   - <% if environment.has_custom_welcome_screen? %>
4   - <%= environment.settings[:signup_welcome_screen_body].html_safe %>
5   - <% elsif environment.enabled?('admin_must_approve_new_users')%>
6   - <h1><%= _("Welcome to %s!") % environment.name %></h1>
7   - <h3><%= _("Thanks for signing up, we're thrilled to have you on our social network!") %></h3>
8   - <p><%= _("Firstly, some tips for getting started:") %></p>
9   - <% unless environment.enabled?('skip_new_user_email_confirmation') %>
10   - <h4><%= _("Confirm your account and wait for admin approvement!") %></h4>
11   - <p><%= _("You should receive a welcome email from us shortly. Please take a second to follow the link within to confirm your account.") %></p>
12   - <p><%= _("You won't appear as %s until your account is confirmed and approved.") % link_to(_('user'), {:controller => :search, :action => :people, :filter => 'more_recent'}, :target => '_blank') %></p>
13   - <% else %>
14   - <h4><%= _("Wait for admin approvement!") %></h4>
15   - <p><%= _("The administrators will evaluate your signup request for approvement.") %></p>
16   - <p><%= _("You won't appear as %s until your account is approved.") % link_to(_('user'), {:controller => :search, :action => :people, :filter => 'more_recent'}, :target => '_blank') %></p>
17   - <% end %>
18   - <h4><%= _("What to do next?") %></h4>
19   - <p><%= _("%s. Upload an avatar and let your friends find you easily :)") % link_to(_('Customize your profile'), {:controller => 'doc', :section => 'user', :topic => 'editing-person-info'}, :target => '_blank') %></p>
20   - <p><%= _("Learn the guidelines. Read the %s for more details on how to use this social network!") % link_to(_('Documentation'), {:controller => 'doc'}, :target => '_blank') %></p>
21   - <p><%= _("%s your Gmail, Yahoo and Hotmail contacts!") % link_to(_('Invite and find'), {:controller => 'doc', :section => 'user', :topic => 'invite-contacts'}, :target => '_blank') %></p>
22   - <p><%= _("Start exploring and have fun!") %></p>
23   - <% else %>
24   - <h1><%= _("Welcome to %s!") % environment.name %></h1>
25   - <h3><%= _("Thanks for signing up, we're thrilled to have you on our social network!") %></h3>
26   - <p><%= _("Firstly, some tips for getting started:") %></p>
27   - <h4><%= _("Confirm your account!") %></h4>
28   - <p><%= _("You should receive a welcome email from us shortly. Please take a second to follow the link within to confirm your account.") %></p>
29   - <p><%= _("You won't appear as %s until your account is confirmed.") % link_to(_('user'), {:controller => :search, :action => :people, :filter => 'more_recent'}, :target => '_blank') %></p>
30   - <h4><%= _("What to do next?") %></h4>
31   - <p><%= _("%s. Upload an avatar and let your friends find you easily :)") % link_to(_('Customize your profile'), {:controller => 'doc', :section => 'user', :topic => 'editing-person-info'}, :target => '_blank') %></p>
32   - <p><%= _("Learn the guidelines. Read the %s for more details on how to use this social network!") % link_to(_('Documentation'), {:controller => 'doc'}, :target => '_blank') %></p>
33   - <p><%= _("%s your Gmail, Yahoo and Hotmail contacts!") % link_to(_('Invite and find'), {:controller => 'doc', :section => 'user', :topic => 'invite-contacts'}, :target => '_blank') %></p>
34   - <p><%= _("Start exploring and have fun!") %></p>
35   - <% end %>
36   - </div>
37   -<% else %>
38   - <h1><%= _('Sign up for %s!') % environment.name %></h1>
39   - <%= render :partial => 'signup_form' %>
40   -<% end %>
  1 +<h1><%= _('Sign up for %s!') % environment.name %></h1>
  2 +<%= render :partial => 'signup_form' %>
... ...
app/views/admin_panel/_signup_welcome_screen.html.erb
1 1 <div class='description'>
2   - <%= _('This text will be showed as a welcome message to users after signup') %><br/><br/>
  2 + <%= _('If you enable this feature on the "Features" section of the Administration Panel, this text will be shown as a welcome message to users after signup.') %>
3 3 </div>
4   -
5 4 <%= labelled_form_field(_('Body'), text_area(:environment, :signup_welcome_screen_body, :cols => 40, :style => 'width: 100%', :class => 'mceEditor')) %>
  5 +
  6 +<div class='description'>
  7 + <%= _('If this content is left blank, the following page will be displayed to the user:') %>
  8 +</div>
  9 +
  10 +<%= render :file => 'home/welcome', :locals => {:default_message => true} %>
... ...
app/views/admin_panel/set_portal_community.html.erb
... ... @@ -18,7 +18,7 @@
18 18 <%= button 'ok', _('Enable'), {:action => 'manage_portal_community', :activate => 1} %>
19 19 <% end %>
20 20 <%= button 'folder', _('Select Portal Folders'), {:action => 'set_portal_folders'} %>
21   - <%= button 'edit', _('Define Amount by Folder'), {:action => 'set_portal_news_amount'} %>
  21 + <%= button 'edit', _('Define news amount on portal'), {:action => 'set_portal_news_amount'} %>
22 22 <%= button 'delete', _('Remove'), { :action => 'unset_portal_community'} %>
23 23 <% end %>
24 24 <% end %>
... ...