Commit 9701ad866bdf8dd8646d3d45e9525f89035d9155
1 parent
d8b660b2
Exists in
master
and in
29 other branches
profile-suggestions: job to generate suggestions
* Added rules for suggestions * Send e-mail to users with the suggestions (ActionItem3234)
Showing
8 changed files
with
317 additions
and
0 deletions
Show diff stats
app/mailers/user_mailer.rb
| ... | ... | @@ -41,6 +41,23 @@ class UserMailer < 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] We have suggestions for 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/profile.rb
| ... | ... | @@ -513,6 +513,14 @@ class Profile < ActiveRecord::Base |
| 513 | 513 | generate_url(:profile => identifier, :controller => 'profile', :action => 'index') |
| 514 | 514 | end |
| 515 | 515 | |
| 516 | + def people_suggestions_url | |
| 517 | + generate_url(:profile => identifier, :controller => 'friends', :action => 'suggest') | |
| 518 | + end | |
| 519 | + | |
| 520 | + def communities_suggestions_url | |
| 521 | + generate_url(:profile => identifier, :controller => 'memberships', :action => 'suggest') | |
| 522 | + end | |
| 523 | + | |
| 516 | 524 | def generate_url(options) |
| 517 | 525 | url_options.merge(options) |
| 518 | 526 | end | ... | ... |
app/models/profile_suggestion.rb
| ... | ... | @@ -29,6 +29,119 @@ class ProfileSuggestion < ActiveRecord::Base |
| 29 | 29 | settings_items category.to_sym |
| 30 | 30 | attr_accessible category.to_sym |
| 31 | 31 | end |
| 32 | + | |
| 33 | + RULES = %w[ | |
| 34 | + friends_of_friends_with_common_friends | |
| 35 | + people_with_common_communities | |
| 36 | + people_with_common_tags | |
| 37 | + communities_with_common_friends | |
| 38 | + communities_with_common_tags | |
| 39 | + ] | |
| 40 | + | |
| 41 | + # Number of suggestions | |
| 42 | + N_SUGGESTIONS = 30 | |
| 43 | + | |
| 44 | + # Number max of attempts | |
| 45 | + MAX_ATTEMPTS = N_SUGGESTIONS * 2 | |
| 46 | + | |
| 47 | + # Number of friends in common | |
| 48 | + COMMON_FRIENDS = 2 | |
| 49 | + | |
| 50 | + # Number of communities in common | |
| 51 | + COMMON_COMMUNITIES = 1 | |
| 52 | + | |
| 53 | + # Number of friends in common | |
| 54 | + COMMON_TAGS = 2 | |
| 55 | + | |
| 56 | + def self.friends_of_friends_with_common_friends(person) | |
| 57 | + person_attempts = 0 | |
| 58 | + person_friends = person.friends | |
| 59 | + person_friends.each do |friend| | |
| 60 | + friend.friends.includes.each do |friend_of_friend| | |
| 61 | + person_attempts += 1 | |
| 62 | + return unless person.profile_suggestions.count < N_SUGGESTIONS && person_attempts < MAX_ATTEMPTS | |
| 63 | + unless friend_of_friend == person || friend_of_friend.is_a_friend?(person) || person.already_request_friendship?(friend_of_friend) | |
| 64 | + common_friends = friend_of_friend.friends & person_friends | |
| 65 | + if common_friends.size >= COMMON_FRIENDS | |
| 66 | + person.profile_suggestions.create(:suggestion => friend_of_friend, :common_friends => common_friends.size) | |
| 67 | + end | |
| 68 | + end | |
| 69 | + end | |
| 70 | + end | |
| 71 | + end | |
| 72 | + | |
| 73 | + def self.people_with_common_communities(person) | |
| 74 | + person_attempts = 0 | |
| 75 | + person_communities = person.communities | |
| 76 | + person_communities.each do |community| | |
| 77 | + community.members.each do |member| | |
| 78 | + person_attempts += 1 | |
| 79 | + return unless person.profile_suggestions.count < N_SUGGESTIONS && person_attempts < MAX_ATTEMPTS | |
| 80 | + unless member == person || member.is_a_friend?(person) || person.already_request_friendship?(member) | |
| 81 | + common_communities = person_communities & member.communities | |
| 82 | + if common_communities.size >= COMMON_COMMUNITIES | |
| 83 | + person.profile_suggestions.create(:suggestion => member, :common_communities => common_communities.size) | |
| 84 | + end | |
| 85 | + end | |
| 86 | + end | |
| 87 | + end | |
| 88 | + end | |
| 89 | + | |
| 90 | + def self.people_with_common_tags(person) | |
| 91 | + person_attempts = 0 | |
| 92 | + tags = person.article_tags.keys | |
| 93 | + tags.each do |tag| | |
| 94 | + person_attempts += 1 | |
| 95 | + return unless person.profile_suggestions.count < N_SUGGESTIONS && person_attempts < MAX_ATTEMPTS | |
| 96 | + tagged_content = ActsAsTaggableOn::Tagging.includes(:taggable).where(:tag_id => ActsAsTaggableOn::Tag.find_by_name(tag)) | |
| 97 | + tagged_content.each do |tg| | |
| 98 | + author = tg.taggable.created_by | |
| 99 | + unless author.nil? || author == person || author.is_a_friend?(person) || person.already_request_friendship?(author) | |
| 100 | + common_tags = tags & author.article_tags.keys | |
| 101 | + if common_tags.size >= COMMON_TAGS | |
| 102 | + person.profile_suggestions.create(:suggestion => author, :common_tags => common_tags.size) | |
| 103 | + end | |
| 104 | + end | |
| 105 | + end | |
| 106 | + end | |
| 107 | + end | |
| 108 | + | |
| 109 | + def self.communities_with_common_friends(person) | |
| 110 | + community_attempts = 0 | |
| 111 | + person_friends = person.friends | |
| 112 | + person_friends.each do |friend| | |
| 113 | + friend.communities.each do |community| | |
| 114 | + community_attempts += 1 | |
| 115 | + return unless person.profile_suggestions.count < N_SUGGESTIONS && community_attempts < MAX_ATTEMPTS | |
| 116 | + unless person.is_member_of?(community) || community.already_request_membership?(person) | |
| 117 | + common_friends = community.members & person.friends | |
| 118 | + if common_friends.size >= COMMON_FRIENDS | |
| 119 | + person.profile_suggestions.create(:suggestion => community, :common_friends => common_friends.size) | |
| 120 | + end | |
| 121 | + end | |
| 122 | + end | |
| 123 | + end | |
| 124 | + end | |
| 125 | + | |
| 126 | + def self.communities_with_common_tags(person) | |
| 127 | + community_attempts = 0 | |
| 128 | + tags = person.article_tags.keys | |
| 129 | + tags.each do |tag| | |
| 130 | + community_attempts += 1 | |
| 131 | + return unless person.profile_suggestions.count < N_SUGGESTIONS && community_attempts < MAX_ATTEMPTS | |
| 132 | + tagged_content = ActsAsTaggableOn::Tagging.includes(:taggable).where(:tag_id => ActsAsTaggableOn::Tag.find_by_name(tag)) | |
| 133 | + tagged_content.each do |tg| | |
| 134 | + profile = tg.taggable.profile | |
| 135 | + unless !profile.community? || person.is_member_of?(profile) || profile.already_request_membership?(person) | |
| 136 | + common_tags = tags & profile.article_tags.keys | |
| 137 | + if common_tags.size >= COMMON_TAGS | |
| 138 | + person.profile_suggestions.create(:suggestion => profile, :common_tags => common_tags.size) | |
| 139 | + end | |
| 140 | + end | |
| 141 | + end | |
| 142 | + end | |
| 143 | + end | |
| 144 | + | |
| 32 | 145 | def disable |
| 33 | 146 | self.enabled = false |
| 34 | 147 | self.save | ... | ... |
app/views/user_mailer/profiles_suggestions_email.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,32 @@ |
| 1 | +<%= _('Hi, %{recipient}!') % { :recipient => @recipient } %> | |
| 2 | + | |
| 3 | +<p><%= _('We want to give you some suggestions to grow up your network. Check it out!') %></p> | |
| 4 | + | |
| 5 | +<% unless @people_suggestions.empty? %> | |
| 6 | + <p><%= _('Friends suggestions:') %></p> | |
| 7 | + | |
| 8 | + <ul> | |
| 9 | + <% @people_suggestions.each do |person| %> | |
| 10 | + <li><a href="<%= url_for(person.public_profile_url) %>"><%= person.name %></a></li> | |
| 11 | + <% end %> | |
| 12 | + <ul> | |
| 13 | + <%= _("To see the full list of friends suggestions, follow the link: %s") % @people_suggestions_url %> | |
| 14 | +<% end %> | |
| 15 | + | |
| 16 | +<% unless @communities_suggestions.empty? %> | |
| 17 | + <p><%= _('Communities suggestions:') %></p> | |
| 18 | + | |
| 19 | + <ul> | |
| 20 | + <% @communities_suggestions.each do |community| %> | |
| 21 | + <li><a href="<%= url_for(community.public_profile_url) %>"><%= community.name %></a></li> | |
| 22 | + <% end %> | |
| 23 | + <ul> | |
| 24 | + <%= _("To see the full list of communities suggestions, follow the link: %s") % @communities_suggestions_url %> | |
| 25 | +<% end %> | |
| 26 | + | |
| 27 | + | |
| 28 | +<%= _("Greetings,") %> | |
| 29 | + | |
| 30 | +-- | |
| 31 | +<%= _('%s team.') % @environment %> | |
| 32 | +<%= @url %> | ... | ... |
| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | +class ProfileSuggestionsJob < Struct.new(:person_id) | |
| 2 | + | |
| 3 | + def perform | |
| 4 | + begin | |
| 5 | + person = Person.find(person_id) | |
| 6 | + | |
| 7 | + ProfileSuggestion::RULES.each do |rule| | |
| 8 | + ProfileSuggestion.send(rule, person) | |
| 9 | + end | |
| 10 | + | |
| 11 | + UserMailer.profiles_suggestions_email(person).deliver | |
| 12 | + rescue Exception => exception | |
| 13 | + Rails.logger.warn("Error with suggestions for person ID %d\n%s" % [person_id, exception.to_s]) | |
| 14 | + end | |
| 15 | + end | |
| 16 | + | |
| 17 | +end | ... | ... |
test/unit/person_test.rb
| ... | ... | @@ -1521,4 +1521,16 @@ class PersonTest < ActiveSupport::TestCase |
| 1521 | 1521 | assert_equal [suggested_community], person.suggested_communities |
| 1522 | 1522 | end |
| 1523 | 1523 | |
| 1524 | + should 'return url to people suggestions for a person' do | |
| 1525 | + environment = create_environment('mycolivre.net') | |
| 1526 | + profile = build(Person, :identifier => 'testprofile', :environment_id => create_environment('mycolivre.net').id) | |
| 1527 | + assert_equal({ :host => "mycolivre.net", :profile => 'testprofile', :controller => 'friends', :action => 'suggest' }, profile.people_suggestions_url) | |
| 1528 | + end | |
| 1529 | + | |
| 1530 | + should 'return url to communities suggestions for a person' do | |
| 1531 | + environment = create_environment('mycolivre.net') | |
| 1532 | + profile = build(Person, :identifier => 'testprofile', :environment_id => create_environment('mycolivre.net').id) | |
| 1533 | + assert_equal({ :host => "mycolivre.net", :profile => 'testprofile', :controller => 'memberships', :action => 'suggest' }, profile.communities_suggestions_url) | |
| 1534 | + end | |
| 1535 | + | |
| 1524 | 1536 | end | ... | ... |
| ... | ... | @@ -0,0 +1,108 @@ |
| 1 | +require File.dirname(__FILE__) + '/../test_helper' | |
| 2 | + | |
| 3 | +class ProfileSuggestionsJobTest < ActiveSupport::TestCase | |
| 4 | + | |
| 5 | + should 'suggest friends from friends' do | |
| 6 | + person = create_user('person').person | |
| 7 | + friend = create_user('friend').person | |
| 8 | + friend2 = create_user('friend2').person | |
| 9 | + friend3 = create_user('friend3').person | |
| 10 | + | |
| 11 | + person.add_friend friend | |
| 12 | + friend.add_friend person | |
| 13 | + | |
| 14 | + person.add_friend friend2 | |
| 15 | + friend2.add_friend person | |
| 16 | + | |
| 17 | + person.add_friend friend3 | |
| 18 | + friend3.add_friend person | |
| 19 | + | |
| 20 | + friend_of_friend = create_user('friend_of_friend').person | |
| 21 | + friend.add_friend friend_of_friend | |
| 22 | + friend_of_friend.add_friend friend | |
| 23 | + | |
| 24 | + friend_of_friend.add_friend friend2 | |
| 25 | + friend2.add_friend friend_of_friend | |
| 26 | + | |
| 27 | + friend_of_friend.add_friend friend3 | |
| 28 | + friend3.add_friend friend_of_friend | |
| 29 | + | |
| 30 | + job = ProfileSuggestionsJob.new(person.id) | |
| 31 | + assert_difference 'ProfileSuggestion.count', 1 do | |
| 32 | + job.perform | |
| 33 | + end | |
| 34 | + assert_equal [friend_of_friend], person.suggested_people | |
| 35 | + end | |
| 36 | + | |
| 37 | + should 'suggest friends from communities' do | |
| 38 | + person = create_user('person').person | |
| 39 | + community = fast_create(Community) | |
| 40 | + | |
| 41 | + member1 = create_user('member1').person | |
| 42 | + member2 = create_user('member2').person | |
| 43 | + | |
| 44 | + community.add_member person | |
| 45 | + community.add_member member1 | |
| 46 | + community.add_member member2 | |
| 47 | + | |
| 48 | + job = ProfileSuggestionsJob.new(person.id) | |
| 49 | + assert_difference 'ProfileSuggestion.count', 2 do | |
| 50 | + job.perform | |
| 51 | + end | |
| 52 | + assert_equivalent [member1, member2], person.suggested_people | |
| 53 | + end | |
| 54 | + | |
| 55 | + should 'suggest friends from tags' do | |
| 56 | + person = create_user('person').person | |
| 57 | + person2 = create_user('person2').person | |
| 58 | + person3 = create_user('person3').person | |
| 59 | + | |
| 60 | + create(Article, :created_by => person, :profile => person, :tag_list => 'first-tag, second-tag') | |
| 61 | + create(Article, :created_by => person2, :profile => person2, :tag_list => 'first-tag, second-tag, third-tag') | |
| 62 | + create(Article, :created_by => person3, :profile => person3, :tag_list => 'first-tag') | |
| 63 | + | |
| 64 | + job = ProfileSuggestionsJob.new(person.id) | |
| 65 | + assert_difference 'ProfileSuggestion.count', 1 do | |
| 66 | + job.perform | |
| 67 | + end | |
| 68 | + assert_equivalent [person2], person.suggested_people | |
| 69 | + end | |
| 70 | + | |
| 71 | + should 'suggest from communities friends' do | |
| 72 | + person = create_user('person').person | |
| 73 | + | |
| 74 | + member1 = create_user('member1').person | |
| 75 | + member2 = create_user('member2').person | |
| 76 | + | |
| 77 | + person.add_friend member1 | |
| 78 | + person.add_friend member2 | |
| 79 | + | |
| 80 | + community = fast_create(Community) | |
| 81 | + community.add_member member1 | |
| 82 | + community.add_member member2 | |
| 83 | + | |
| 84 | + job = ProfileSuggestionsJob.new(person.id) | |
| 85 | + assert_difference 'ProfileSuggestion.count', 1 do | |
| 86 | + job.perform | |
| 87 | + end | |
| 88 | + assert_equivalent [community], person.suggested_communities | |
| 89 | + end | |
| 90 | + | |
| 91 | + should 'suggest communities from tags' do | |
| 92 | + person = create_user('person').person | |
| 93 | + person2 = create_user('person2').person | |
| 94 | + | |
| 95 | + community = fast_create(Community) | |
| 96 | + community.add_admin person2 | |
| 97 | + | |
| 98 | + create(Article, :created_by => person, :profile => person, :tag_list => 'first-tag, second-tag') | |
| 99 | + create(Article, :created_by => person2, :profile => community, :tag_list => 'first-tag, second-tag, third-tag') | |
| 100 | + | |
| 101 | + job = ProfileSuggestionsJob.new(person.id) | |
| 102 | + assert_difference 'ProfileSuggestion.count', 1 do | |
| 103 | + job.perform | |
| 104 | + end | |
| 105 | + assert_equivalent [community], person.suggested_communities | |
| 106 | + end | |
| 107 | + | |
| 108 | +end | ... | ... |
test/unit/profile_test.rb
| ... | ... | @@ -1955,4 +1955,14 @@ class ProfileTest < ActiveSupport::TestCase |
| 1955 | 1955 | p = fast_create(Profile) |
| 1956 | 1956 | assert p.folder_types.include?('ProfileTest::Folder1') |
| 1957 | 1957 | end |
| 1958 | + | |
| 1959 | + should 'disable suggestion if profile requested membership' do | |
| 1960 | + person = fast_create(Person) | |
| 1961 | + community = fast_create(Community) | |
| 1962 | + suggestion = ProfileSuggestion.create(:person => person, :suggestion => community, :enabled => true) | |
| 1963 | + | |
| 1964 | + community.add_member person | |
| 1965 | + assert_equal false, ProfileSuggestion.find(suggestion.id).enabled | |
| 1966 | + end | |
| 1967 | + | |
| 1958 | 1968 | end | ... | ... |