Commit 0409f85d20e1e0a8652d1a49c8ff93cc85f48658

Authored by Rodrigo Souto
2 parents 7b118e80 024d804a

Merge branch 'invite-user-to-community' of https://gitlab.com/larissa/noosfero into invite-user

Conflicts:
	app/views/friends/index.rhtml
	app/views/invite/_select_address_book.rhtml
	app/views/invite/select_friends.rhtml
	app/views/profile/friends.rhtml
	config/routes.rb
	plugins/stoa/lib/ext/person.rb
	test/functional/invite_controller_test.rb
	test/unit/invite_member_test.rb
app/controllers/application_controller.rb
... ... @@ -182,7 +182,7 @@ class ApplicationController < ActionController::Base
182 182 private
183 183  
184 184 def fallback_find_by_contents(asset, scope, query, paginate_options, options)
185   - scope = scope.like_search(query) unless query.blank?
  185 + scope = scope.like_search(query, options) unless query.blank?
186 186 scope = scope.send(options[:filter]) unless options[:filter].blank?
187 187 {:results => scope.paginate(paginate_options)}
188 188 end
... ...
app/controllers/public/invite_controller.rb
... ... @@ -4,8 +4,15 @@ class InviteController < 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] }
  12 + last = labels.pop
  13 + label = labels.join(', ')
  14 + @search_friend_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 < 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,14 +59,35 @@ class InviteController < 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 + return
  76 + else
  77 + redirect_to :action => 'invite_friends'
  78 + session[:notice] = _('Please enter a valid profile.')
  79 + end
  80 + end
  81 +
  82 + def search_friend
  83 + render :text => find_by_contents(:people, environment.people.not_members_of(profile), params['q'], {:page => 1}, {:joins => :user})[:results].map {|person| {:id => person.id, :name => person.name} }.to_json
56 84 end
57 85  
58 86 protected
59 87  
60 88 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)
  89 + if profile.person? and !user.has_permission?(:manage_friends, profile) or
  90 + profile.community? and !user.has_permission?(:invite_members, profile)
63 91 render_access_denied
64 92 end
65 93 end
... ...
app/models/article.rb
... ... @@ -7,11 +7,11 @@ class Article < ActiveRecord::Base
7 7 acts_as_having_image
8 8  
9 9 SEARCHABLE_FIELDS = {
10   - :name => 10,
11   - :abstract => 3,
12   - :body => 2,
13   - :slug => 1,
14   - :filename => 1,
  10 + :name => {:label => _('Name'), :weight => 10},
  11 + :abstract => {:label => _('Abstract'), :weight => 3},
  12 + :body => {:label => _('Content'), :weight => 2},
  13 + :slug => {:label => _('Slug'), :weight => 1},
  14 + :filename => {:label => _('Filename'), :weight => 1},
15 15 }
16 16  
17 17 SEARCH_FILTERS = %w[
... ...
app/models/category.rb
... ... @@ -3,10 +3,10 @@ class Category < 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 < 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/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
... ...
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   - elsif !user.person.is_a_friend?(person)
  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/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/person.rb
... ... @@ -21,6 +21,12 @@ class Person &lt; Profile
21 21 { :select => 'DISTINCT profiles.*', :joins => :role_assignments, :conditions => [conditions] }
22 22 }
23 23  
  24 + scope :not_members_of, lambda { |resources|
  25 + resources = [resources] if !resources.kind_of?(Array)
  26 + conditions = resources.map {|resource| "role_assignments.resource_type = '#{resource.class.base_class.name}' AND role_assignments.resource_id = #{resource.id || -1}"}.join(' OR ')
  27 + { :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] }
  28 + }
  29 +
24 30 def has_permission_with_plugins?(permission, profile)
25 31 permissions = [has_permission_without_plugins?(permission, profile)]
26 32 permissions += plugins.map do |plugin|
... ...
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 8 SEARCH_FILTERS = %w[
... ...
app/models/profile.rb
... ... @@ -12,9 +12,9 @@ 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 20 SEARCH_FILTERS = %w[
... ... @@ -629,6 +629,7 @@ private :generate_url, :url_options
629 629 self.affiliate(person, Profile::Roles.admin(environment.id)) if members.count == 0
630 630 self.affiliate(person, Profile::Roles.member(environment.id))
631 631 end
  632 + person.tasks.pending.of("InviteMember").select { |t| t.data[:community_id] == self.id }.each { |invite| invite.cancel }
632 633 else
633 634 raise _("%s can't have members") % self.class.name
634 635 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
... ...
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
... ...
app/views/friends/index.html.erb
... ... @@ -15,7 +15,7 @@
15 15 <%= button(:back, _('Back to control panel'), :controller => 'profile_editor') %>
16 16 <%= button(:search, _('Find people'), :controller => 'search', :action => 'assets', :asset => 'people') %>
17 17 <% unless @plugins.dispatch(:remove_invite_friends_button).include?(true) %>
18   - <%= button(:search, _('Invite people from my e-mail contacts'), :controller => 'invite', :action => 'select_address_book') %>
  18 + <%= button(:search, _('Invite people from my e-mail contacts'), :controller => 'invite', :action => 'invite_friends') %>
19 19 <% end %>
20 20 <% end %>
21 21 <% end %>
... ... @@ -46,7 +46,7 @@
46 46 <%= button(:back, _('Back to control panel'), :controller => 'profile_editor') %>
47 47 <%= button(:search, _('Find people'), :controller => 'search', :action => 'assets', :asset => 'people') %>
48 48 <% unless @plugins.dispatch(:remove_invite_friends_button).include?(true) %>
49   - <%= button(:search, _('Invite people from my e-mail contacts'), :controller => 'invite', :action => 'select_address_book') %>
  49 + <%= button(:search, _('Invite people from my e-mail contacts'), :controller => 'invite', :action => 'invite_friends') %>
50 50 <% end %>
51 51 <% end %>
52 52 <% end %>
... ...
app/views/invite/_personalize_invitation_mail.rhtml 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +<br/>
  2 +
  3 +<%= link_to ('Personalize invitation mail'), nil, :onclick => "jQuery('#invitation-mail_template').show(); return false;" %>
  4 +
  5 +<div id='invitation-mail_template' style='display:none'>
  6 + <%= h _("Now enter an invitation message. You must keep the <url> code in your invitation text. When your friends receive the invitation e-mail, <url> will be replaced by a link that they need to click to activate their account. <user> and <friend> codes will be replaced by your name and friend name, but they are optional.") %>
  7 + <%= labelled_form_field(_('Invitation text:'), text_area_tag(:mail_template, mail_template, :cols => 72, :rows => 8)) %>
  8 +</div>
... ...
app/views/invite/_select_address_book.html.erb 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +<% header ||='h2' %>
  2 +<<%= header %>><%= _('Step 1 of 2: Select address book') %></<%= header %>>
  3 +
  4 +<%= form_tag do %>
  5 +
  6 + <%= [
  7 + radio_button_tag(:import_from, "manual", @import_from == "manual", :onclick => 'hide_invite_friend_login_password()') + content_tag('label', _('Manually (empty field)'), :for => "import_from_manual"),
  8 + radio_button_tag(:import_from, "gmail", @import_from == "gmail", :onclick => 'show_invite_friend_login_password(this.value)') + content_tag('label', 'Gmail', :for => 'import_from_gmail'),
  9 + radio_button_tag(:import_from, "yahoo", @import_from == "yahoo", :onclick => 'show_invite_friend_login_password(this.value)') + content_tag('label', 'Yahoo', :for => "import_from_yahoo"),
  10 + radio_button_tag(:import_from, "hotmail", @import_from == "hotmail", :onclick => 'show_invite_friend_login_password(this.value)') + content_tag('label', 'Hotmail', :for => "import_from_hotmail")
  11 + ].join("\n<br/>\n") %>
  12 +
  13 + <script type="text/javascript">
  14 + function hide_invite_friend_login_password() {
  15 + $('invite-friends-login-password').hide();
  16 + }
  17 + function show_invite_friend_login_password(option) {
  18 + if (option == 'hotmail') {
  19 + $('hotmail_username_tip').show();
  20 + } else {
  21 + $('hotmail_username_tip').hide();
  22 + }
  23 + $('invite-friends-login-password').show();
  24 + $('login').focus();
  25 + }
  26 + </script>
  27 + <div id='invite-friends-login-password' <%= "style='display: none;'" if (@import_from == 'manual') %>>
  28 + <div id='hotmail_username_tip'>
  29 + <%= ui_icon('ui-icon-alert') %>
  30 + <%= _('Please type your username in the format yourname@example.com') %>
  31 + </div>
  32 +
  33 + <%= labelled_form_field(_("Username") + ":", text_field_tag(:login, @login)) %>
  34 + <%= labelled_form_field(_("Password") + ":", password_field_tag(:password)) %>
  35 + </div>
  36 +
  37 + <% button_bar do %>
  38 + <%= submit_button(:forward, _("Next")) %>
  39 + <% end %>
  40 + <p><%= _("We won't store your password or contact anyone without your permission.") %></p>
  41 +<% end %>
... ...
app/views/invite/invite_friends.rhtml 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +<% if profile.person? %>
  2 + <h1><%= _('Invite your friends') %></h1>
  3 +<% else %>
  4 + <h1><%= _('Invite your friends to join %s') % profile.name %></h1>
  5 +<% end %>
  6 +
  7 +<% unless profile.person? %>
  8 + <h2><%= _('Invite other registered users') %></h2>
  9 + <p><%= _('You can search for user profiles and invite them to join this community.') %></p>
  10 + <% form_tag :action => 'invite_registered_friend' do %>
  11 + <% search_action = url_for(:action => 'search_friend') %>
  12 + <%= token_input_field_tag(:q, 'search-friends', search_action,
  13 + { :hint_text => _('Type in your friend\'s ') + @search_friend_fields,
  14 + :focus => false }) %>
  15 +
  16 + <% button_bar do %>
  17 + <%= submit_button('save', _('Invite'))%>
  18 + <%= button('cancel', _('Cancel'), profile.url)%>
  19 + <% end %>
  20 + <% end %>
  21 +
  22 + <br />
  23 + <h2><%= _('Invite people from my e-mail contacts') %></h2>
  24 + <% header = 'h3' %>
  25 +
  26 +<% end %>
  27 +
  28 +<%= render :partial => 'invite/select_address_book', :locals => {:header => header} %>
  29 +
  30 +<div id="loadingScreen"></div>
... ...
app/views/invite/select_address_book.html.erb
... ... @@ -1,51 +0,0 @@
1   -<% if profile.person? %>
2   - <h1><%= _('Invite your friends') %></h1>
3   -<% else %>
4   - <h1><%= _('Invite your friends to join %s') % profile.name %></h1>
5   -<% end %>
6   -
7   -<h2><%= _('Step 1 of 2: Select address book') %></h2>
8   -
9   -<%= form_tag do %>
10   -
11   - <%= [
12   - radio_button_tag(:import_from, "manual", @import_from == "manual", :onclick => 'hide_invite_friend_login_password()') + content_tag('label', _('Manually (empty field)'), :for => "import_from_manual"),
13   - radio_button_tag(:import_from, "gmail", @import_from == "gmail", :onclick => 'show_invite_friend_login_password(this.value)') + content_tag('label', 'Gmail', :for => 'import_from_gmail'),
14   - radio_button_tag(:import_from, "yahoo", @import_from == "yahoo", :onclick => 'show_invite_friend_login_password(this.value)') + content_tag('label', 'Yahoo', :for => "import_from_yahoo"),
15   - radio_button_tag(:import_from, "hotmail", @import_from == "hotmail", :onclick => 'show_invite_friend_login_password(this.value)') + content_tag('label', 'Hotmail', :for => "import_from_hotmail")
16   - ].join("\n<br/>\n") %>
17   -
18   - <script type="text/javascript">
19   - function hide_invite_friend_login_password() {
20   - $('invite-friends-login-password').hide();
21   - }
22   - function show_invite_friend_login_password(option) {
23   - if (option == 'hotmail') {
24   - $('hotmail_username_tip').show();
25   - } else {
26   - $('hotmail_username_tip').hide();
27   - }
28   - $('invite-friends-login-password').show();
29   - $('login').focus();
30   - }
31   - </script>
32   - <div id='invite-friends-login-password' <%= "style='display: none;'" if (@import_from == 'manual') %>>
33   - <div id='hotmail_username_tip'>
34   - <%= ui_icon('ui-icon-alert') %>
35   - <%= _('Please type your username in the format yourname@example.com') %>
36   - </div>
37   -
38   - <%= labelled_form_field(_("Username") + ":", text_field_tag(:login, @login)) %>
39   - <%= labelled_form_field(_("Password") + ":", password_field_tag(:password)) %>
40   - </div>
41   -
42   - <% button_bar do %>
43   - <%= submit_button(:forward, _("Next")) %>
44   - <% end %>
45   - <p><%= _("We won't store your password or contact anyone without your permission.") %></p>
46   -<% end %>
47   -
48   -<div id="loadingScreen"></div>
49   -
50   -
51   -
app/views/invite/select_friends.html.erb
... ... @@ -9,7 +9,7 @@
9 9  
10 10 <h2><%= _('Step 2 of 2: Selecting Friends') %></h2>
11 11  
12   -<%= button(:back, _('Back'), { :action => 'select_address_book' }, :id => 'invitation_back_button') %>
  12 +<%= button(:back, _('Back'), { :action => 'invite_friends' }, :id => 'invitation_back_button') %>
13 13  
14 14 <p>
15 15 <%= _('Indicate which friends you want to invite.') %>
... ... @@ -30,14 +30,7 @@
30 30 </div>
31 31 <% end -%>
32 32  
33   - <br/>
34   -
35   - <%= link_to ('Personalize invitation mail'), nil, :onclick => "jQuery('#invitation-mail_template').show(); return false;" %>
36   -
37   - <div id='invitation-mail_template' style='display:none'>
38   - <%= h _("Now enter an invitation message. You must keep the <url> code in your invitation text. When your friends receive the invitation e-mail, <url> will be replaced by a link that they need to click to activate their account. <user> and <friend> codes will be replaced by your name and friend name, but they are optional.") %>
39   - <%= labelled_form_field(_('Invitation text:'), text_area_tag(:mail_template, @mail_template, :cols => 72, :rows => 8)) %>
40   - </div>
  33 + <%= render :partial => 'invite/personalize_invitation_mail', :locals => {:mail_template => @mail_template } %>
41 34  
42 35 <% button_bar do %>
43 36 <%= submit_button(:ok, _("Invite my friends!")) %>
... ...
app/views/profile/friends.html.erb
... ... @@ -18,7 +18,7 @@
18 18 <%= button :back, _('Go back'), { :controller => 'profile' } %>
19 19 <% if user == profile %>
20 20 <%= button :edit, _('Manage my friends'), :controller => 'friends', :action => 'index', :profile => profile.identifier %>
21   - <%= button(:search, _('Invite people from my e-mail contacts'), :controller => 'invite', :action => 'select_address_book') %>
  21 + <%= button(:search, _('Invite people from my e-mail contacts'), :controller => 'invite', :action => 'invite_friends') %>
22 22 <% end %>
23 23 <% end %>
24 24  
... ...
app/views/profile/members.html.erb
... ... @@ -18,7 +18,7 @@
18 18 <%= button :back, _('Go back'), { :controller => 'profile' } %>
19 19 <% if profile.community? and user %>
20 20 <% if user.has_permission?(:invite_members, profile) %>
21   - <%= button :search, _('Invite your friends to join %s') % profile.name, :controller => 'invite', :action => 'select_address_book' %>
  21 + <%= button :search, _('Invite your friends to join %s') % profile.name, :controller => 'invite', :action => 'invite_friends' %>
22 22 <% end %>
23 23 <% if user.has_permission?(:send_mail_to_members, profile) %>
24 24 <%= button :send, _('Send e-mail to members'), :controller => 'profile', :action => 'send_mail' %>
... ...
app/views/profile_members/_index_buttons.html.erb
... ... @@ -2,7 +2,7 @@
2 2 <%= button :back, _('Back'), :controller => 'profile_editor' %>
3 3 <%= button :add, _('Add members'), :action => 'add_members' if profile.enterprise? %>
4 4 <% if profile.community? and user.has_permission?(:invite_members, profile) %>
5   - <%= button :search, _('Invite your friends to join %s') % profile.short_name, :controller => 'invite', :action => 'select_address_book' %>
  5 + <%= button :search, _('Invite your friends to join %s') % profile.short_name, :controller => 'invite', :action => 'invite_friends' %>
6 6 <% end %>
7 7 <% if profile.community? and user.has_permission?(:send_mail_to_members, profile) %>
8 8 <%= button :send, _('Send e-mail to members'), :controller => 'profile', :action => 'send_mail' %>
... ...
config/routes.rb
... ... @@ -67,7 +67,7 @@ Noosfero::Application.routes.draw do
67 67 match 'catalog/:profile', :controller => 'catalog', :action => 'index', :profile => /#{Noosfero.identifier_format}/, :as => :catalog
68 68  
69 69 # invite
70   - match 'profile/:profile/invite/friends', :controller => 'invite', :action => 'select_address_book', :profile => /#{Noosfero.identifier_format}/
  70 + match 'profile/:profile/invite/friends', :controller => 'invite', :action => 'invite_friends', :profile => /#{Noosfero.identifier_format}/
71 71 match 'profile/:profile/invite/:action', :controller => 'invite', :profile => /#{Noosfero.identifier_format}/
72 72  
73 73 # feeds per tag
... ...
lib/noosfero/core_ext/active_record.rb
... ... @@ -13,14 +13,32 @@ class ActiveRecord::Base
13 13 key.join('/')
14 14 end
15 15  
16   - def self.like_search(query)
17   - if defined?(self::SEARCHABLE_FIELDS)
18   - fields = self::SEARCHABLE_FIELDS.keys.map(&:to_s) & column_names
  16 + def self.like_search(query, options={})
  17 + if defined?(self::SEARCHABLE_FIELDS) || options[:fields].present?
  18 + fields_per_table = {}
  19 + fields_per_table[table_name] = (options[:fields].present? ? options[:fields] : self::SEARCHABLE_FIELDS.keys.map(&:to_s)) & column_names
  20 +
  21 + if options[:joins].present?
  22 + join_asset = options[:joins].to_s.classify.constantize
  23 + if defined?(join_asset::SEARCHABLE_FIELDS) || options[:fields].present?
  24 + fields_per_table[join_asset.table_name] = (options[:fields].present? ? options[:fields] : join_asset::SEARCHABLE_FIELDS.keys.map(&:to_s)) & join_asset.column_names
  25 + end
  26 + end
  27 +
19 28 query = query.downcase.strip
20   - conditions = fields.map do |field|
21   - "lower(#{table_name}.#{field}) LIKE '%#{query}%'"
  29 + fields_per_table.delete_if { |table,fields| fields.blank? }
  30 + conditions = fields_per_table.map do |table,fields|
  31 + fields.map do |field|
  32 + "lower(#{table}.#{field}) LIKE '%#{query}%'"
  33 + end.join(' OR ')
22 34 end.join(' OR ')
23   - where(conditions)
  35 +
  36 + if options[:joins].present?
  37 + joins(options[:joins]).where(conditions)
  38 + else
  39 + where(conditions)
  40 + end
  41 +
24 42 else
25 43 raise "No searchable fields defined for #{self.name}"
26 44 end
... ...
plugins/stoa/lib/ext/person.rb
... ... @@ -3,6 +3,8 @@ require_dependency &#39;person&#39;
3 3 class Person
4 4 attr_accessible :usp_id, :invitation_code
5 5  
  6 + SEARCHABLE_FIELDS[:usp_id] = {:label => _('USP Number'), :weight => 5}
  7 +
6 8 validates_uniqueness_of :usp_id, :allow_nil => true
7 9 settings_items :invitation_code
8 10 validate :usp_id_or_invitation, :if => lambda { |person| person.environment && person.environment.plugin_enabled?(StoaPlugin)}
... ...
plugins/stoa/lib/stoa_plugin.rb
... ... @@ -110,7 +110,7 @@ class StoaPlugin &lt; Noosfero::Plugin
110 110 { :title => _('Invite friends'),
111 111 :icon => 'invite-friends',
112 112 :url => {:controller => 'invite',
113   - :action => 'select_address_book'} } if context.send(:user) && context.send(:user).usp_id.present?
  113 + :action => 'invite_friends'} } if context.send(:user) && context.send(:user).usp_id.present?
114 114 end
115 115  
116 116 def remove_invite_friends_button
... ... @@ -121,4 +121,8 @@ class StoaPlugin &lt; Noosfero::Plugin
121 121 {:field => :usp_id, :name => _('USP Number'), :model => 'person'}
122 122 end
123 123  
  124 + def search_friend_fields
  125 + [{:field => :usp_id, :name => _('USP Number')}]
  126 + end
  127 +
124 128 end
... ...
plugins/stoa/test/functional/invite_controller_test.rb
... ... @@ -20,7 +20,7 @@ class InviteControllerTest &lt; ActionController::TestCase
20 20 person_without_usp_id = User.create!(:login => 'user-without', :email => 'user-without@example.com', :password => 'test', :password_confirmation => 'test', :person_data => {:invitation_code => 12345678}).person
21 21  
22 22 login_as(person_without_usp_id.identifier)
23   - get :select_address_book, :profile => person_without_usp_id.identifier
  23 + get :invite_friends, :profile => person_without_usp_id.identifier
24 24 assert_response 403
25 25 get :select_friends, :profile => person_without_usp_id.identifier
26 26 assert_response 403
... ... @@ -30,7 +30,7 @@ class InviteControllerTest &lt; ActionController::TestCase
30 30 person_with_usp_id = User.create!(:login => 'user-with', :email => 'user-with@example.com', :password => 'test', :password_confirmation => 'test', :person_data => {:usp_id => 12345678}).person
31 31  
32 32 login_as(person_with_usp_id.identifier)
33   - get :select_address_book, :profile => person_with_usp_id.identifier
  33 + get :invite_friends, :profile => person_with_usp_id.identifier
34 34 assert_response 200
35 35 get :select_friends, :profile => person_with_usp_id.identifier, :contact_list => ContactList.create.id
36 36 assert_response 200
... ... @@ -42,11 +42,23 @@ class InviteControllerTest &lt; ActionController::TestCase
42 42 organization.add_admin(person_with_usp_id)
43 43  
44 44 login_as(person_with_usp_id.identifier)
45   - get :select_address_book, :profile => organization.identifier
  45 + get :invite_friends, :profile => organization.identifier
46 46 assert_response 200
47 47 get :select_friends, :profile => organization.identifier, :contact_list => ContactList.create.id
48 48 assert_response 200
49 49 end
50 50  
  51 + should 'search friends profiles by usp_id' do
  52 + person1 = User.create!(:login => 'john', :email => 'john@example.com', :password => 'test', :password_confirmation => 'test', :person_data => {:usp_id => 12345678}).person
  53 + User.create!(:login => 'mary', :email => 'mary@example.com', :password => 'test', :password_confirmation => 'test', :person_data => {:usp_id => 11111111}).person
  54 + organization = fast_create(Organization)
  55 + organization.add_admin(person1)
  56 +
  57 + login_as(person1.identifier)
  58 + get :search_friend, :profile => organization.identifier, :q => '1234'
  59 +
  60 + assert_equal [{"name" => person1.name, "id" => person1.id}].to_json, @response.body
  61 + assert_response 200
  62 + end
51 63 end
52 64  
... ...
test/functional/invite_controller_test.rb
... ... @@ -99,7 +99,7 @@ class InviteControllerTest &lt; ActionController::TestCase
99 99 end
100 100  
101 101 should 'display invitation page' do
102   - get :select_address_book, :profile => profile.identifier
  102 + get :invite_friends, :profile => profile.identifier
103 103 assert_response :success
104 104 assert_tag :tag => 'h1', :content => 'Invite your friends'
105 105 end
... ... @@ -118,8 +118,8 @@ class InviteControllerTest &lt; ActionController::TestCase
118 118 assert_equal InviteFriend.mail_template, assigns(:mail_template)
119 119 end
120 120  
121   - should 'deny select_address_book f user has no rights to invite members' do
122   - get :select_address_book, :profile => community.identifier
  121 + should 'deny invite_friends if user has no rights to invite members' do
  122 + get :invite_friends, :profile => community.identifier
123 123 assert_response 403 # forbidden
124 124 end
125 125  
... ... @@ -128,13 +128,13 @@ class InviteControllerTest &lt; ActionController::TestCase
128 128 assert_response 403 # forbidden
129 129 end
130 130  
131   - should 'deny select_address_book access when trying to invite friends to another user' do
132   - get :select_address_book, :profile => friend.identifier
  131 + should 'deny invite_friends access when trying to invite friends to another user' do
  132 + get :invite_friends, :profile => friend.identifier
133 133 assert_response 403 # forbidden
134 134 end
135 135  
136 136 should 'deny select_friends access when trying to invite friends to another user' do
137   - get :select_address_book, :profile => friend.identifier
  137 + get :select_friends, :profile => friend.identifier
138 138 assert_response 403 # forbidden
139 139 end
140 140  
... ... @@ -155,7 +155,7 @@ class InviteControllerTest &lt; ActionController::TestCase
155 155 community.add_admin(profile)
156 156 contact_list = ContactList.create
157 157 assert_difference 'Delayed::Job.count', 1 do
158   - post :select_address_book, :profile => community.identifier, :contact_list => contact_list.id, :import_from => 'gmail'
  158 + post :invite_friends, :profile => community.identifier, :contact_list => contact_list.id, :import_from => 'gmail'
159 159 end
160 160 end
161 161  
... ... @@ -223,7 +223,7 @@ class InviteControllerTest &lt; ActionController::TestCase
223 223 assert_difference 'ContactList.count', -1 do
224 224 get :cancel_fetching_emails, :profile => profile.identifier, :contact_list => contact_list.id
225 225 end
226   - assert_redirected_to :action => 'select_address_book'
  226 + assert_redirected_to :action => 'invite_friends'
227 227 end
228 228  
229 229 should 'set locale in the background job' do
... ... @@ -235,6 +235,55 @@ class InviteControllerTest &lt; ActionController::TestCase
235 235 assert_equal 'pt', job.payload_object.locale
236 236 end
237 237  
  238 + should 'search friends profiles by name, email or identifier' do
  239 + friend1 = create_user('willy').person
  240 + friend2 = create_user('william').person
  241 + friend1.name = 'cris'
  242 + friend2.email = 'me@example.com'
  243 + friend1.save
  244 + friend2.save
  245 +
  246 + get :search_friend, :profile => profile.identifier, :q => 'me@'
  247 +
  248 + assert_equal 'text/html', @response.content_type
  249 + assert_equal [{"name" => friend2.name, "id" => friend2.id}].to_json, @response.body
  250 +
  251 + get :search_friend, :profile => profile.identifier, :q => 'cri'
  252 +
  253 + assert_equal [{"name" => friend1.name, "id" => friend1.id}].to_json, @response.body
  254 +
  255 + get :search_friend, :profile => profile.identifier, :q => 'will'
  256 +
  257 + assert_equal [{"name" => friend1.name, "id" => friend1.id}, {"name" => friend2.name, "id" => friend2.id}].to_json, @response.body
  258 + end
  259 +
  260 + should 'not include members in search friends profiles' do
  261 + community.add_admin(profile)
  262 + friend1 = create_user('willy').person
  263 + friend2 = create_user('william').person
  264 + friend1.save
  265 + friend2.save
  266 +
  267 + community.add_member(friend2)
  268 +
  269 + get :search_friend, :profile => community.identifier, :q => 'will'
  270 +
  271 + assert_equal [{"name" => friend1.name, "id" => friend1.id}].to_json, @response.body
  272 + end
  273 +
  274 + should 'invite registered users through profile id' do
  275 + friend1 = create_user('testuser1').person
  276 + friend2 = create_user('testuser2').person
  277 + assert_difference Delayed::Job, :count, 1 do
  278 + post :invite_registered_friend, :profile => profile.identifier, :q => "#{friend1.id},#{friend2.id}", :mail_template => "click: <url>"
  279 + assert_redirected_to :controller => 'profile', :action => 'friends'
  280 + end
  281 +
  282 + assert_difference InviteFriend, :count, 2 do
  283 + process_delayed_job_queue
  284 + end
  285 + end
  286 +
238 287 private
239 288  
240 289 def json_response
... ...
test/integration/routing_test.rb
... ... @@ -213,7 +213,7 @@ class RoutingTest &lt; ActionController::IntegrationTest
213 213 end
214 214  
215 215 def test_invite_routing
216   - assert_routing('/profile/colivre/invite/friends', :controller => 'invite', :action => 'select_address_book', :profile => 'colivre')
  216 + assert_routing('/profile/colivre/invite/friends', :controller => 'invite', :action => 'invite_friends', :profile => 'colivre')
217 217 end
218 218  
219 219 def test_chat_routing
... ...
test/unit/invitation_test.rb
... ... @@ -72,6 +72,29 @@ class InvitationTest &lt; ActiveSupport::TestCase
72 72 end
73 73 end
74 74  
  75 + should 'do nothing if the invited friend is already your friend' do
  76 + person = create_user('person').person
  77 + invited_friend = create_user('invited_friend').person
  78 +
  79 + invited_friend.add_friend(person)
  80 +
  81 + assert_no_difference InviteFriend, :count do
  82 + Invitation.invite( person, [invited_friend.user.email], "", person )
  83 + end
  84 + end
  85 +
  86 + should 'and yet be able to invite friends to community' do
  87 + person = create_user('person').person
  88 + invited_friend = create_user('invited_friend').person
  89 +
  90 + invited_friend.add_friend(person)
  91 + community = fast_create(Community)
  92 +
  93 + assert_difference InviteMember, :count do
  94 + Invitation.invite( person, [invited_friend.user.email], "", community )
  95 + end
  96 + end
  97 +
75 98 should 'add url on message if user removed it' do
76 99 person = create_user('testuser1').person
77 100 friend = create_user('testuser2').person
... ... @@ -97,4 +120,18 @@ class InvitationTest &lt; ActiveSupport::TestCase
97 120 should 'have a message with url' do
98 121 assert_equal "\n\nTo accept invitation, please follow this link: <url>", Invitation.default_message_to_accept_invitation
99 122 end
  123 +
  124 + should 'invite friends through profile id' do
  125 + person = create_user('testuser1').person
  126 + friend = create_user('testuser2').person
  127 + community = fast_create(Community)
  128 +
  129 + assert_difference InviteMember, :count do
  130 + Invitation.invite(person, [friend.id.to_s], 'hello friend <url>', community)
  131 + end
  132 + assert_difference InviteFriend, :count do
  133 + Invitation.invite(person, [friend.id.to_s], 'hello friend <url>', person)
  134 + end
  135 + end
  136 +
100 137 end
... ...
test/unit/invite_friend_test.rb
... ... @@ -140,4 +140,16 @@ class InviteFriendTest &lt; ActiveSupport::TestCase
140 140 assert_match(/#{task.requestor.name} wants to be your friend./, email.subject)
141 141 end
142 142  
  143 + should 'not invite friends if there is a pending invitation' do
  144 + person = create_user('testuser1').person
  145 + friend = create_user('testuser2').person
  146 +
  147 + assert_difference 'InviteFriend.count' do
  148 + InviteFriend.create({:person => person, :target => friend})
  149 + end
  150 +
  151 + assert_no_difference 'InviteFriend.count' do
  152 + InviteFriend.create({:person => person, :target => friend})
  153 + end
  154 + end
143 155 end
... ...
test/unit/invite_member_test.rb
... ... @@ -22,6 +22,20 @@ class InviteMemberTest &lt; ActiveSupport::TestCase
22 22 ok('friend is member of community') { community.members.include?(friend) }
23 23 end
24 24  
  25 + should 'cancel other invitations for same community when confirmed' do
  26 + friend = create_user('friend').person
  27 + p1 = create_user('testuser1').person
  28 + p2 = create_user('testuser2').person
  29 + community = fast_create(Community)
  30 +
  31 + task = InviteMember.create!(:person => p1, :friend => friend, :community_id => community.id)
  32 + InviteMember.create!(:person => p2, :friend => friend, :community_id => community.id)
  33 +
  34 + assert_difference friend.tasks.pending, :count, -2 do
  35 + task.finish
  36 + end
  37 + end
  38 +
25 39 should 'require community (person inviting other to be a member)' do
26 40 task = InviteMember.new
27 41 task.valid?
... ... @@ -78,11 +92,11 @@ class InviteMemberTest &lt; ActiveSupport::TestCase
78 92 task = InviteMember.create!(:person => p1, :friend_email => 'test@test.com', :message => '<url>', :community_id => fast_create(Community).id)
79 93 end
80 94  
81   - should 'not send e-mails to friend if target given (person being invited)' do
  95 + should 'send e-mails notification to friend if target given (person being invited)' do
82 96 p1 = create_user('testuser1').person
83 97 p2 = create_user('testuser2').person
84 98  
85   - TaskMailer.expects(:deliver_invitation_notification).never
  99 + TaskMailer.expects(:deliver_target_notification).once
86 100  
87 101 task = InviteMember.create!(:person => p1, :friend => p2, :community_id => fast_create(Community).id)
88 102 end
... ... @@ -117,7 +131,7 @@ class InviteMemberTest &lt; ActiveSupport::TestCase
117 131 assert_match(/#{task.requestor.name} invited you to join #{community.name}/, email.subject)
118 132 end
119 133  
120   - should 'destroy InviteMember task when the community is destroyed' do
  134 + should 'destroy InviteMember task when the community is destroyed' do
121 135 p1 = create_user('testuser1').person
122 136 p2 = create_user('testuser2').person
123 137 p3 = create_user('testuser3').person
... ... @@ -131,5 +145,42 @@ class InviteMemberTest &lt; ActiveSupport::TestCase
131 145 assert_raise ActiveRecord::RecordNotFound do; t2.reload; end
132 146 end
133 147  
134   -end
  148 + should 'have target notification message only if target given (person being invited)' do
  149 + p1 = create_user('testuser1').person
  150 + p2 = create_user('testuser2').person
  151 +
  152 + task = InviteMember.create!(:person => p1, :friend => p2, :community_id => fast_create(Community).id)
  153 + assert_nothing_raised NotImplementedError do
  154 + task.target_notification_message
  155 + end
  156 +
  157 + task = InviteMember.create!(:person => p1, :friend_email => 'test@test.com', :message => '<url>', :community_id => fast_create(Community).id)
  158 + assert_raise NotImplementedError do
  159 + task.target_notification_message
  160 + end
  161 + end
  162 +
  163 + should 'deliver target notification message if target given (person being invited)' do
  164 + p1 = create_user('testuser1').person
  165 + p2 = create_user('testuser2').person
  166 +
  167 + task = InviteMember.create!(:person => p1, :friend => p2, :community_id => fast_create(Community).id)
  168 +
  169 + email = TaskMailer.deliver_target_notification(task, task.target_notification_message)
  170 + assert_match(/#{task.requestor.name} invited you to join #{task.community.name}/, email.subject)
  171 + end
  172 +
  173 + should 'not invite member if there is a pending invitation' do
  174 + person = create_user('testuser1').person
  175 + friend = create_user('testuser2').person
  176 + community = fast_create(Community)
135 177  
  178 + assert_difference 'InviteMember.count' do
  179 + InviteMember.create({:person => person, :target => friend, :community_id => community.id})
  180 + end
  181 +
  182 + assert_no_difference 'InviteMember.count' do
  183 + InviteMember.create({:person => person, :target => friend, :community_id => community.id})
  184 + end
  185 + end
  186 +end
... ...
test/unit/person_test.rb
... ... @@ -1118,6 +1118,15 @@ class PersonTest &lt; ActiveSupport::TestCase
1118 1118 assert_equal [person], Person.members_of(community)
1119 1119 end
1120 1120  
  1121 + should 'return unique non-members of a community' do
  1122 + member = fast_create(Person)
  1123 + person = fast_create(Person)
  1124 + community = fast_create(Community)
  1125 + community.add_member(member)
  1126 +
  1127 + assert_equal (Person.all - Person.members_of(community)).sort, Person.not_members_of(community).sort
  1128 + end
  1129 +
1121 1130 should 'be able to pass array to members_of' do
1122 1131 person1 = fast_create(Person)
1123 1132 community = fast_create(Community)
... ... @@ -1130,6 +1139,20 @@ class PersonTest &lt; ActiveSupport::TestCase
1130 1139 assert_includes Person.members_of([community, enterprise]), person2
1131 1140 end
1132 1141  
  1142 + should 'be able to pass array to not_members_of' do
  1143 + person1 = fast_create(Person)
  1144 + community = fast_create(Community)
  1145 + community.add_member(person1)
  1146 + person2 = fast_create(Person)
  1147 + enterprise = fast_create(Enterprise)
  1148 + enterprise.add_member(person2)
  1149 + person3 = fast_create(Person)
  1150 +
  1151 + assert_not_includes Person.not_members_of([community, enterprise]), person1
  1152 + assert_not_includes Person.not_members_of([community, enterprise]), person2
  1153 + assert_includes Person.not_members_of([community, enterprise]), person3
  1154 + end
  1155 +
1133 1156 should 'find more active people' do
1134 1157 Person.destroy_all
1135 1158 p1 = fast_create(Person)
... ...