Commit 7332d0d429d673aa611264155d5df5d93988798d

Authored by Rodrigo Souto
2 parents eec520b6 6259b1af

Merge remote-tracking branch 'dani/ActionItem3234' into stoa-merge

Conflicts:
	app/controllers/my_profile/memberships_controller.rb
	app/models/profile.rb
	app/views/friends/index.html.erb
	test/unit/profile_test.rb
Showing 42 changed files with 1274 additions and 214 deletions   Show diff stats
INSTALL.chat.md
1 -XMPP/Chat Client Setup  
2 -====================== 1 +XMPP/Chat Setup
  2 +===============
3 3
4 -To configure XMPP/BOSH in Noosfero you need: 4 +The samples of config file to configure a XMPP/BOSH server with ejabberd,
  5 +postgresql and apache2 can be found at util/chat directory.
5 6
6 -* REST Client - http://github.com/archiloque/rest-client  
7 -* SystemTimer - http://ph7spot.com/musings/system-timer  
8 -* Pidgin data files - http://www.pidgin.im/ 7 +This setup supposes that you are using Noosfero installed via Debian package
  8 +in a production environment.
9 9
10 -If you use Debian 6.0 (squeeze):  
11 -  
12 - # apt-get install librestclient-ruby pidgin-data ruby1.8-dev  
13 - # gem install SystemTimer  
14 -  
15 -The samples of config file to configure a XMPP/BOSH server with ejabberd, postgresql and apache2 can be found at util/chat directory.  
16 -  
17 -XMPP/Chat Server Setup  
18 -====================== 10 +Steps
  11 +=====
19 12
20 This is a step-by-step guide to get a XMPP service working, in a Debian system. 13 This is a step-by-step guide to get a XMPP service working, in a Debian system.
21 14
22 ## 1. Install the required packages 15 ## 1. Install the required packages
23 16
24 - # apt-get install ejabberd odbc-postgresql 17 + # apt-get install ejabberd odbc-postgresql librestclient-ruby pidgin-data ruby1.8-dev
  18 + # gem install SystemTimer
25 19
26 ## 2. Ejabberd configuration 20 ## 2. Ejabberd configuration
27 21
28 -All the following changes must be done in config file: `/etc/ejabberd/ejabberd.cfg`  
29 -  
30 -### 2.1. Set the default admin user  
31 -  
32 - { acl, admin, { user, "john", "www.example.com" } }.  
33 - { acl, admin, { user, "bart", "www.example.com" } }.  
34 -  
35 -### 2.2. Set the default host  
36 -  
37 - { hosts, [ "www.example.com" ] }.  
38 -  
39 -### 2.3. Http-Bind activation  
40 -  
41 - { 5280, ejabberd_http, [  
42 - http_bind,  
43 - web_admin  
44 - ]  
45 - }  
46 -  
47 - (...)  
48 -  
49 - { modules, [  
50 - {mod_http_bind, []},  
51 - ...  
52 - ] }.  
53 -  
54 -Ejabberd creates semi-anonymous rooms by default, but Noosfero's Jabber client needs non-anonymous room, then we need to change default params of creation rooms in ejabberd to create non-anonymous rooms.  
55 -  
56 -In non-anonymous rooms the jabber service sends the new occupant's full JID to all occupants in the room [[1]]. 22 + # cp /usr/share/noosfero/util/chat/ejabberd.cfg /etc/ejabberd/
57 23
58 -Add option "`{default_room_options, [{anonymous, false}]}`" to `/etc/ejabberd/ejabberd.cfg` in mod_muc session. See below: 24 +Edit the /etc/ejabberd/ejabberd.cfg file and set your domain on the first 2 lines.
59 25
60 - { mod_muc, [  
61 - %%{host, "conference.@HOST@"},  
62 - {access, muc},  
63 - {access_create, muc},  
64 - {access_persistent, muc},  
65 - {access_admin, muc_admin},  
66 - {max_users, 500},  
67 - {default_room_options, [{anonymous, false}]}  
68 - ]},  
69 -  
70 -[1]: http://xmpp.org/extensions/xep-0045.html#enter-nonanon  
71 -  
72 -  
73 -### 2.4. Authentication method  
74 -  
75 -To use Postgresql through ODBC, the following modifications must be done:  
76 -  
77 - * Disable the default method:  
78 - `{auth_method, internal}.`  
79 -  
80 - * Enable autheticantion through ODBC:  
81 - `{auth_method, odbc}.` 26 +## 3. Configuring Postgresql
82 27
83 - * Set database server name  
84 - `{odbc_server, "DSN=PostgreSQLEjabberdNoosfero"}.` 28 +Give permission to noosfero user create new roles, login as
  29 +postgres user and execute:
85 30
  31 + $ psql
  32 + postgres=# GRANT CREATE ON DATABASE noosfero TO noosfero;
86 33
87 -### 2.5. Increase the shaper traffic limit 34 +Change the postgresql authentication method to md5 instead of ident,
  35 +add the following line to the file /etc/postgresql/8.4/main/pg_hba.conf:
88 36
89 - { shaper, normal, { maxrate, 10000000 } }. 37 + # Noosfero user
  38 + local noosfero noosfero md5
90 39
  40 +(add this line before the following line)
91 41
92 -### 2.6. Disable unused modules 42 + # "local" is for Unix domain socket connections only
  43 + local all all ident
93 44
94 -Unused modules can be disabled, for example: 45 +Restart postgresql server:
95 46
96 - * s2s  
97 - * web_admin  
98 - * mod_pubsub  
99 - * mod_irc  
100 - * mod_offline  
101 - * mod_admin_extra  
102 - * mod_register 47 + # service postgresql restart
103 48
  49 +Login as noosfero user, and execute:
104 50
105 -### 2.7. Enable ODBC modules 51 + $ psql -U noosfero -W noosfero < /usr/share/noosfero/util/chat/postgresql/ejabberd.sql
106 52
107 - * mod_privacy -> mod_privacy_odbc  
108 - * mod_private -> mod_private_odbc  
109 - * mod_roster -> mod_roster_odbc 53 +(see database password in the /etc/noosfero/database.yml file)
110 54
111 -## 3. Configuring Postgresql 55 +This will create a new schema inside the noosfero database, called `ejabberd`.
112 56
113 -Login as noosfero user, and execute: 57 +Note that there should be at least one domain with `is_default = true` in
  58 +`domains` table, otherwise people won't be able to see their friends online.
114 59
115 - $ psql noosfero < /path/to/noosfero/util/chat/postgresql/ejabberd.sql 60 +## 4. ODBC configuration
116 61
117 -Where `noosfero` may need to be replace by the name of the database used for Noosfero. 62 +Create the following files:
118 63
119 -This will create a new schema inside the noosfero database, called `ejabberd`. 64 + # cp /usr/share/noosfero/util/chat/odbc.ini /etc/
  65 + # cp /usr/share/noosfero/util/chat/odbcinst.ini /etc/
120 66
121 -Note `noosfero` user should have permission to create Postgresql schemas. Also, there should be at least one domain with `is_default = true` in `domains` table, otherwise people won't be able to see their friends online. 67 +Edit the odbc.ini file and set the password for the database user, see
  68 +the file /etc/noosfero/database.yml to get the password.
122 69
123 -## 4. ODBC configuration 70 +Adjust premissions:
124 71
125 -The following files must be created:  
126 -  
127 -`/etc/odbc.ini`:  
128 -  
129 - [PostgreSQLEjabberdNoosfero]  
130 - Description = PostgreSQL Noosfero ejabberd database  
131 - Driver = PostgreSQL Unicode  
132 - Trace = No  
133 - TraceFile = /tmp/psqlodbc.log  
134 - Database = noosfero  
135 - Servername = localhost  
136 - UserName = <DBUSER>  
137 - Password = <DBPASS>  
138 - Port =  
139 - ReadOnly = No  
140 - RowVersioning = No  
141 - ShowSystemTables = No  
142 - ShowOidColumn = No  
143 - FakeOidIndex = No  
144 - ConnSettings = SET search_path TO ejabberd  
145 -  
146 -`/etc/odbcinst.ini`:  
147 -  
148 - [PostgreSQL Unicode]  
149 - Description = PostgreSQL ODBC driver (Unicode version)  
150 - Driver = /usr/lib/odbc/psqlodbcw.so  
151 - Setup = /usr/lib/odbc/libodbcpsqlS.so  
152 - Debug = 0  
153 - CommLog = 1  
154 - UsageCount = 3 72 + # chmod 640 /etc/odbc.ini
  73 + # chown ejabberd /etc/odbc.ini
155 74
156 ## 4.1 testing all: 75 ## 4.1 testing all:
157 76
@@ -159,7 +78,6 @@ The following files must be created: @@ -159,7 +78,6 @@ The following files must be created:
159 78
160 If the configuration was done right, the message "Connected!" will be displayed. 79 If the configuration was done right, the message "Connected!" will be displayed.
161 80
162 -  
163 ## 5. Enabling kernel polling and SMP in `/etc/default/ejabberd` 81 ## 5. Enabling kernel polling and SMP in `/etc/default/ejabberd`
164 82
165 POLL=true 83 POLL=true
@@ -205,32 +123,45 @@ Note: module proxy_http must be enabled: @@ -205,32 +123,45 @@ Note: module proxy_http must be enabled:
205 123
206 # a2enmod proxy_http 124 # a2enmod proxy_http
207 125
208 -## 8. DNS configuration 126 +Restart services:
209 127
210 -For this point, we assume you are using BIND as your DNS server. You need to add the following entries to the DNS zone file corresponding to the domain of your noosfero site: 128 + # service ejabberd restart
  129 + # service noosfero restart
  130 + # service apache2 restart
211 131
212 - _xmpp-client._tcp SRV 5 100 5222 master  
213 - conference CNAME master  
214 - _xmpp-client._tcp.conference SRV 5 100 5222 master 132 +## 8. Test Apache Configuration
215 133
216 -If you are running a DNS server other than BIND, you will have to figure out how to create equivalente rules for your zone file. Patches to this documentation are welcome. 134 +Open in your browser the address:
217 135
218 -## 9. Testing this Setup 136 + http://<yout domain>/http-bind
219 137
220 -Adjust shell limits to proceed with some benchmarks and load tests: 138 +You should see a page with a message like that:
221 139
222 - # ulimit −s 256  
223 - # ulimit −n 8192  
224 - # echo 10 > /proc/sys/net/ipv4/tcp_syn_retries 140 + ejabberd mod_http_bind
  141 + An implementation of XMPP over BOSH (XEP-0206)
  142 + This web page is only informative. To use HTTP-Bind you need a Jabber/XMPP
  143 + client that supports it.
225 144
226 -To measure the bandwidth between server and client: 145 +## 9. Test chat session
227 146
228 - * at server side:  
229 - `# iperf −s` 147 +Open Noosfero console and execute:
230 148
231 - * at client side:  
232 - `# iperf −c server_ip` 149 +>> environment = Environment.default
  150 +>> user = Person['guest']
  151 +>> password = user.user.crypted_password
  152 +>> login = user.jid
  153 +>> RubyBOSH.initialize_session(login, password, "http://#{environment.default_hostname}/http-bind", :wait => 30, :hold => 1, :window => 5
233 154
234 -For heavy load tests, clone and use this software: 155 +If you have luck, should see something like that:
235 156
236 - $ git clone http://git.holoscopio.com/git/metal/tester.git 157 +Ruby-BOSH - SEND
  158 +<body window="5" rid="60265" xmlns="http://jabber.org/protocol/httpbind" xmlns:xmpp="urn:xmpp:xbosh" to="vagrant-debian-squeeze.vagrantup.com" wait="30" xmpp:version="1.0" hold="1"/>
  159 +Ruby-BOSH - SEND
  160 +<body rid="60266" xmlns="http://jabber.org/protocol/httpbind" sid="24cdfc43646a2af1059a7060b677c2e11b26f34f" xmlns:xmpp="urn:xmpp:xbosh" xmpp:version="1.0"><auth mechanism="PLAIN" xmlns="urn:ietf:params:xml:ns:xmpp-sasl">Z3Vlc3RAdmFncmFudC1kZWJpYW4tc3F1ZWV6ZS52YWdyYW50dXAuY29tAGd1ZXN0ADEzZTFhYWVlYjRhYjZlMTA0MmRkNWI1YWY0MzM4MjA1OGJiOWZmNzk=</auth></body>
  161 +Ruby-BOSH - SEND
  162 +<body xmpp:restart="true" rid="60267" xmlns="http://jabber.org/protocol/httpbind" sid="24cdfc43646a2af1059a7060b677c2e11b26f34f" xmlns:xmpp="urn:xmpp:xbosh" xmpp:version="1.0"/>
  163 +Ruby-BOSH - SEND
  164 +<body rid="60268" xmlns="http://jabber.org/protocol/httpbind" sid="24cdfc43646a2af1059a7060b677c2e11b26f34f" xmlns:xmpp="urn:xmpp:xbosh" xmpp:version="1.0"><iq type="set" xmlns="jabber:client" id="bind_29330"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>bosh_9631</resource></bind></iq></body>
  165 +Ruby-BOSH - SEND
  166 +<body rid="60269" xmlns="http://jabber.org/protocol/httpbind" sid="24cdfc43646a2af1059a7060b677c2e11b26f34f" xmlns:xmpp="urn:xmpp:xbosh" xmpp:version="1.0"><iq type="set" xmlns="jabber:client" id="sess_21557"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq></body>
  167 +=> ["guest@vagrant-debian-squeeze.vagrantup.com", "24cdfc43646a2af1059a7060b677c2e11b26f34f", 60270]
INSTALL.https.md 0 → 100644
@@ -0,0 +1,115 @@ @@ -0,0 +1,115 @@
  1 +Setup Noosfero to use HTTPS
  2 +===========================
  3 +
  4 +This document assumes that you have a fully and clean Noosfero
  5 +installation as explained at the `INSTALL.md` file.
  6 +
  7 +SSL certificate
  8 ++++++++++++++++
  9 +
  10 +You should get a valid SSL certificate, but if you want to test
  11 +your setup before, you could generate a self-signed certificate
  12 +as below:
  13 +
  14 + # mkdir /etc/noosfero/ssl
  15 + # cd /etc/noosfero/ssl
  16 + # openssl genrsa 1024 > noosfero.key
  17 + # openssl req -new -x509 -nodes -sha1 -days $[10*365] -key noosfero.key > noosfero.cert
  18 + # cat noosfero.key noosfero.cert > noosfero.pem
  19 +
  20 +There are two ways of using SSL with Noosfero: 1) If you are not using
  21 +Varnish; and 2) If you are using Varnish.
  22 +
  23 +1) If you are are not using Varnish
  24 ++++++++++++++++++++++++++++++++++++
  25 +
  26 +Simply do a redirect in apache to force all connections with SSL:
  27 +
  28 + <VirtualHost *:8080>
  29 + ServerName test.stoa.usp.br
  30 +
  31 + Redirect / https://example.com/
  32 + </VirtualHost>
  33 +
  34 +And set a vhost to receive then:
  35 +
  36 + <VirtualHost *:443>
  37 + ServerName example.com
  38 +
  39 + SSLEngine On
  40 + SSLCertificateFile /etc/ssl/certs/cert.pem
  41 + SSLCertificateKeyFile /etc/ssl/private/cert.key
  42 +
  43 + Include /etc/noosfero/apache/virtualhost.conf
  44 + </VirtualHost>
  45 +
  46 +Be aware that if you had configured varnish, the requests won't reach
  47 +it with this configuration.
  48 +
  49 +2) If you are using Varnish
  50 ++++++++++++++++++++++++++++
  51 +
  52 +Varnish isn't able to communicate with the SSL protocol, so we will
  53 +need some one who do this and Pound[1] can do the job. In order to
  54 +install it in Debian based systems:
  55 +
  56 + $ sudo apt-get install pound
  57 +
  58 +Set Varnish to listen in other port than 80:
  59 +
  60 +/etc/defaults/varnish
  61 +---------------------
  62 +
  63 + DAEMON_OPTS="-a localhost:6081 \
  64 + -T localhost:6082 \
  65 + -f /etc/varnish/default.vcl \
  66 + -S /etc/varnish/secret \
  67 + -s file,/var/lib/varnish/$INSTANCE/varnish_storage.bin,1G"
  68 +
  69 +Configure Pound:
  70 +
  71 + # cp /usr/share/noosfero/etc/pound.cfg /etc/pound/
  72 +
  73 +Edit /etc/pound.cfg and set the IP and domain of your server.
  74 +
  75 +Configure Pound to start at system initialization:
  76 +
  77 +/etc/default/pound
  78 +------------------
  79 +
  80 + startup=1
  81 +
  82 +Set Apache to only listen to localhost:
  83 +
  84 +/etc/apache2/ports.conf
  85 +-----------------------
  86 +
  87 + Listen 127.0.0.1:8080
  88 +
  89 +Restart the services:
  90 +
  91 + $ sudo service apache2 restart
  92 + $ sudo service varnish restart
  93 +
  94 +Start pound:
  95 +
  96 + $ sudo service pound start
  97 +
  98 +[1] http://www.apsis.ch/pound
  99 +
  100 +Noosfero XMPP chat
  101 +++++++++++++++++++
  102 +
  103 +If you want to use chat over HTTPS, then you should add the domain
  104 +and IP of your server in the /etc/hosts file, example:
  105 +
  106 +/etc/hosts
  107 +----------
  108 +
  109 + 192.168.1.86 mydomain.example.com
  110 +
  111 +Also, it's recomended that you remove lines above from the file
  112 +`/etc/apache2/sites-enabled/noosfero`:
  113 +
  114 + RewriteEngine On
  115 + Include /usr/share/noosfero/util/chat/apache/xmpp.conf
INSTALL.varnish.md
@@ -24,10 +24,6 @@ Install the RPAF apache module (or skip this step if not using apache): @@ -24,10 +24,6 @@ Install the RPAF apache module (or skip this step if not using apache):
24 24
25 3b) Edit `/etc/apache2/sites-enabled/*`, and change `<VirtualHost *:80>` to `<VirtualHost *:8080>` 25 3b) Edit `/etc/apache2/sites-enabled/*`, and change `<VirtualHost *:80>` to `<VirtualHost *:8080>`
26 26
27 -3c) Restart apache  
28 -  
29 - # invoke-rc.d apache2 restart  
30 -  
31 4) Varnish configuration 27 4) Varnish configuration
32 28
33 4a) Edit `/etc/default/varnish` 29 4a) Edit `/etc/default/varnish`
@@ -44,10 +40,6 @@ On manual installations, change `/etc/noosfero/*` to `{Rails.root}/etc/noosfero/ @@ -44,10 +40,6 @@ On manual installations, change `/etc/noosfero/*` to `{Rails.root}/etc/noosfero/
44 40
45 **NOTE**: it is very important that the `*.vcl` files are included in that order, i.e. *first* include `varnish-noosfero.vcl`, and *after* `noosfero-accept-language.cvl`. 41 **NOTE**: it is very important that the `*.vcl` files are included in that order, i.e. *first* include `varnish-noosfero.vcl`, and *after* `noosfero-accept-language.cvl`.
46 42
47 -4c) Restart Varnish  
48 -  
49 - # invoke-rc.d varnish restart  
50 -  
51 5) Enable varnish logging: 43 5) Enable varnish logging:
52 44
53 5a) Edit `/etc/default/varnishncsa` and uncomment the line that contains: 45 5a) Edit `/etc/default/varnishncsa` and uncomment the line that contains:
@@ -56,8 +48,10 @@ On manual installations, change `/etc/noosfero/*` to `{Rails.root}/etc/noosfero/ @@ -56,8 +48,10 @@ On manual installations, change `/etc/noosfero/*` to `{Rails.root}/etc/noosfero/
56 48
57 The varnish log will be written to `/var/log/varnish/varnishncsa.log` in an apache-compatible format. You should change your statistics generation software (e.g. awstats) to use that instead of apache logs. 49 The varnish log will be written to `/var/log/varnish/varnishncsa.log` in an apache-compatible format. You should change your statistics generation software (e.g. awstats) to use that instead of apache logs.
58 50
59 -5b) Restart Varnish Logging service 51 +Thanks to Cosimo Streppone for varnish-accept-language. See http://github.com/cosimo/varnish-accept-language for more information.
60 52
61 - # invoke-rc.d varnishncsa restart 53 +6) Restart services
62 54
63 -Thanks to Cosimo Streppone for varnish-accept-language. See http://github.com/cosimo/varnish-accept-language for more information. 55 + # service apache2 restart
  56 + # service varnish restart
  57 + # service varnishncsa restart
app/controllers/my_profile/friends_controller.rb
@@ -3,6 +3,7 @@ class FriendsController &lt; MyProfileController @@ -3,6 +3,7 @@ class FriendsController &lt; MyProfileController
3 protect 'manage_friends', :profile 3 protect 'manage_friends', :profile
4 4
5 def index 5 def index
  6 + @suggestions = profile.suggested_people.limit(per_page/2)
6 if is_cache_expired?(profile.manage_friends_cache_key(params)) 7 if is_cache_expired?(profile.manage_friends_cache_key(params))
7 @friends = profile.friends.paginate(:per_page => per_page, :page => params[:npage]) 8 @friends = profile.friends.paginate(:per_page => per_page, :page => params[:npage])
8 end 9 end
@@ -16,6 +17,21 @@ class FriendsController &lt; MyProfileController @@ -16,6 +17,21 @@ class FriendsController &lt; MyProfileController
16 end 17 end
17 end 18 end
18 19
  20 + def suggest
  21 + @suggestions = profile.suggested_people.paginate(:per_page => per_page, :page => params[:npage])
  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 + suggestion = profile.profile_suggestions.find_by_suggestion_id @person.id
  29 + suggestion.disable
  30 + session[:notice] = _('Suggestion removed')
  31 + redirect_to :action => 'suggest'
  32 + end
  33 + end
  34 +
19 protected 35 protected
20 36
21 class << self 37 class << self
app/controllers/my_profile/memberships_controller.rb
@@ -7,9 +7,9 @@ class MembershipsController &lt; MyProfileController @@ -7,9 +7,9 @@ class MembershipsController &lt; MyProfileController
7 ra = profile.role_assignments.find_by_role_id(role.id) 7 ra = profile.role_assignments.find_by_role_id(role.id)
8 ra.present? && ra.resource_type == 'Profile' 8 ra.present? && ra.resource_type == 'Profile'
9 end 9 end
10 - @filter = params[:filter_type].blank? ? nil : params[:filter_type] 10 + @filter = params[:filter_type].to_i
11 begin 11 begin
12 - @memberships = @filter.nil? ? profile.memberships : profile.memberships_by_role(environment.roles.find(@filter)) 12 + @memberships = @filter.zero? ? profile.memberships : profile.memberships_by_role(environment.roles.find(@filter))
13 rescue ActiveRecord::RecordNotFound 13 rescue ActiveRecord::RecordNotFound
14 @memberships = [] 14 @memberships = []
15 end 15 end
@@ -38,4 +38,19 @@ class MembershipsController &lt; MyProfileController @@ -38,4 +38,19 @@ class MembershipsController &lt; MyProfileController
38 @community = Community.find(params[:community_id]) 38 @community = Community.find(params[:community_id])
39 @back_to = params[:back_to] 39 @back_to = params[:back_to]
40 end 40 end
  41 +
  42 + def suggest
  43 + @suggestions = profile.suggested_communities.paginate(:per_page => 8, :page => params[:npage])
  44 + end
  45 +
  46 + def remove_suggestion
  47 + @community = profile.suggested_communities.find_by_identifier(params[:id])
  48 + redirect_to :action => 'suggest' unless @community
  49 + if @community && request.post?
  50 + suggestion = profile.profile_suggestions.find_by_suggestion_id @community.id
  51 + suggestion.disable
  52 + redirect_to :action => 'suggest'
  53 + end
  54 + end
  55 +
41 end 56 end
app/mailers/user_mailer.rb
@@ -41,6 +41,23 @@ class UserMailer &lt; ActionMailer::Base @@ -41,6 +41,23 @@ class UserMailer &lt; ActionMailer::Base
41 ) 41 )
42 end 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 class Job < Struct.new(:user, :method) 61 class Job < Struct.new(:user, :method)
45 def perform 62 def perform
46 UserMailer.send(method, user).deliver 63 UserMailer.send(method, user).deliver
app/models/add_friend.rb
@@ -14,6 +14,11 @@ class AddFriend &lt; Task @@ -14,6 +14,11 @@ class AddFriend &lt; Task
14 alias :friend :target 14 alias :friend :target
15 alias :friend= :target= 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 def perform 22 def perform
18 target.add_friend(requestor, group_for_friend) 23 target.add_friend(requestor, group_for_friend)
19 requestor.add_friend(target, group_for_person) 24 requestor.add_friend(target, group_for_person)
@@ -48,4 +53,8 @@ class AddFriend &lt; Task @@ -48,4 +53,8 @@ class AddFriend &lt; Task
48 {:type => :profile_image, :profile => requestor, :url => requestor.url} 53 {:type => :profile_image, :profile => requestor, :url => requestor.url}
49 end 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 end 60 end
app/models/person.rb
@@ -66,6 +66,10 @@ class Person &lt; Profile @@ -66,6 +66,10 @@ class Person &lt; Profile
66 has_and_belongs_to_many :acepted_forums, :class_name => 'Forum', :join_table => 'terms_forum_people' 66 has_and_belongs_to_many :acepted_forums, :class_name => 'Forum', :join_table => 'terms_forum_people'
67 has_and_belongs_to_many :articles_with_access, :class_name => 'Article', :join_table => 'article_privacy_exceptions' 67 has_and_belongs_to_many :articles_with_access, :class_name => 'Article', :join_table => 'article_privacy_exceptions'
68 68
  69 + has_many :profile_suggestions, :foreign_key => :person_id, :dependent => :destroy
  70 + has_many :suggested_people, :through => :profile_suggestions, :source => :suggestion, :conditions => ['profile_suggestions.suggestion_type = ? AND profile_suggestions.enabled = ?', 'Person', true]
  71 + has_many :suggested_communities, :through => :profile_suggestions, :source => :suggestion, :conditions => ['profile_suggestions.suggestion_type = ? AND profile_suggestions.enabled = ?', 'Community', true]
  72 +
69 scope :more_popular, :order => 'friends_count DESC' 73 scope :more_popular, :order => 'friends_count DESC'
70 74
71 scope :abusers, :joins => :abuse_complaints, :conditions => ['tasks.status = 3'], :select => 'DISTINCT profiles.*' 75 scope :abusers, :joins => :abuse_complaints, :conditions => ['tasks.status = 3'], :select => 'DISTINCT profiles.*'
app/models/profile.rb
@@ -227,6 +227,8 @@ class Profile &lt; ActiveRecord::Base @@ -227,6 +227,8 @@ class Profile &lt; ActiveRecord::Base
227 227
228 has_many :abuse_complaints, :foreign_key => 'requestor_id', :dependent => :destroy 228 has_many :abuse_complaints, :foreign_key => 'requestor_id', :dependent => :destroy
229 229
  230 + has_many :profile_suggestions, :foreign_key => :suggestion_id, :dependent => :destroy
  231 +
230 def top_level_categorization 232 def top_level_categorization
231 ret = {} 233 ret = {}
232 self.profile_categorizations.each do |c| 234 self.profile_categorizations.each do |c|
@@ -521,6 +523,14 @@ class Profile &lt; ActiveRecord::Base @@ -521,6 +523,14 @@ class Profile &lt; ActiveRecord::Base
521 generate_url(:profile => identifier, :controller => 'profile', :action => 'index') 523 generate_url(:profile => identifier, :controller => 'profile', :action => 'index')
522 end 524 end
523 525
  526 + def people_suggestions_url
  527 + generate_url(:profile => identifier, :controller => 'friends', :action => 'suggest')
  528 + end
  529 +
  530 + def communities_suggestions_url
  531 + generate_url(:profile => identifier, :controller => 'memberships', :action => 'suggest')
  532 + end
  533 +
524 def generate_url(options) 534 def generate_url(options)
525 url_options.merge(options) 535 url_options.merge(options)
526 end 536 end
@@ -645,6 +655,7 @@ private :generate_url, :url_options @@ -645,6 +655,7 @@ private :generate_url, :url_options
645 self.affiliate(person, Profile::Roles.member(environment.id)) 655 self.affiliate(person, Profile::Roles.member(environment.id))
646 end 656 end
647 person.tasks.pending.of("InviteMember").select { |t| t.data[:community_id] == self.id }.each { |invite| invite.cancel } 657 person.tasks.pending.of("InviteMember").select { |t| t.data[:community_id] == self.id }.each { |invite| invite.cancel }
  658 + remove_from_suggestion_list person
648 else 659 else
649 raise _("%s can't have members") % self.class.name 660 raise _("%s can't have members") % self.class.name
650 end 661 end
@@ -973,4 +984,10 @@ private :generate_url, :url_options @@ -973,4 +984,10 @@ private :generate_url, :url_options
973 def preferred_login_redirection 984 def preferred_login_redirection
974 redirection_after_login.blank? ? environment.redirection_after_login : redirection_after_login 985 redirection_after_login.blank? ? environment.redirection_after_login : redirection_after_login
975 end 986 end
  987 +
  988 + def remove_from_suggestion_list(person)
  989 + suggestion = person.profile_suggestions.find_by_suggestion_id self.id
  990 + suggestion.disable if suggestion
  991 + end
  992 +
976 end 993 end
app/models/profile_suggestion.rb 0 → 100644
@@ -0,0 +1,160 @@ @@ -0,0 +1,160 @@
  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, :similarity, :enabled
  6 +
  7 + before_create do |profile_suggestion|
  8 + profile_suggestion.suggestion_type = self.suggestion.class.to_s
  9 + end
  10 +
  11 + acts_as_having_settings :field => :categories
  12 +
  13 + validate :must_be_a_valid_category, :on => :create
  14 + def must_be_a_valid_category
  15 + if categories.keys.map { |cat| self.respond_to?(cat)}.include?(false)
  16 + errors.add(:categories, 'Category must be valid')
  17 + end
  18 + end
  19 +
  20 + validates_uniqueness_of :suggestion_id, :scope => [ :person_id ]
  21 +
  22 + CATEGORIES = {
  23 + :common_friends => _('Friends in common'),
  24 + :common_communities => _('Communities in common'),
  25 + :common_tags => _('Tags in common')
  26 + }
  27 +
  28 + CATEGORIES.keys.each do |category|
  29 + settings_items category.to_sym
  30 + attr_accessible category.to_sym
  31 + end
  32 +
  33 + RULES = %w[
  34 + friends_of_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(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 + suggestion = person.profile_suggestions.find_or_initialize_by_suggestion_id(friend_of_friend.id)
  67 + suggestion.common_friends = common_friends.size
  68 + suggestion.save
  69 + end
  70 + end
  71 + end
  72 + end
  73 + end
  74 +
  75 + def self.people_with_common_communities(person)
  76 + person_attempts = 0
  77 + person_communities = person.communities
  78 + person_communities.each do |community|
  79 + community.members.each do |member|
  80 + person_attempts += 1
  81 + return unless person.profile_suggestions.count < N_SUGGESTIONS && person_attempts < MAX_ATTEMPTS
  82 + unless member == person || member.is_a_friend?(person) || person.already_request_friendship?(member)
  83 + common_communities = person_communities & member.communities
  84 + if common_communities.size >= COMMON_COMMUNITIES
  85 + suggestion = person.profile_suggestions.find_or_initialize_by_suggestion_id(member.id)
  86 + suggestion.common_communities = common_communities.size
  87 + suggestion.save
  88 + end
  89 + end
  90 + end
  91 + end
  92 + end
  93 +
  94 + def self.people_with_common_tags(person)
  95 + person_attempts = 0
  96 + tags = person.article_tags.keys
  97 + tags.each do |tag|
  98 + person_attempts += 1
  99 + return unless person.profile_suggestions.count < N_SUGGESTIONS && person_attempts < MAX_ATTEMPTS
  100 + tagged_content = ActsAsTaggableOn::Tagging.includes(:taggable).where(:tag_id => ActsAsTaggableOn::Tag.find_by_name(tag))
  101 + tagged_content.each do |tg|
  102 + author = tg.taggable.created_by
  103 + unless author.nil? || author == person || author.is_a_friend?(person) || person.already_request_friendship?(author)
  104 + common_tags = tags & author.article_tags.keys
  105 + if common_tags.size >= COMMON_TAGS
  106 + suggestion = person.profile_suggestions.find_or_initialize_by_suggestion_id(author.id)
  107 + suggestion.common_tags = common_tags.size
  108 + suggestion.save
  109 + end
  110 + end
  111 + end
  112 + end
  113 + end
  114 +
  115 + def self.communities_with_common_friends(person)
  116 + community_attempts = 0
  117 + person_friends = person.friends
  118 + person_friends.each do |friend|
  119 + friend.communities.each do |community|
  120 + community_attempts += 1
  121 + return unless person.profile_suggestions.count < N_SUGGESTIONS && community_attempts < MAX_ATTEMPTS
  122 + unless person.is_member_of?(community) || community.already_request_membership?(person)
  123 + common_friends = community.members & person.friends
  124 + if common_friends.size >= COMMON_FRIENDS
  125 + suggestion = person.profile_suggestions.find_or_initialize_by_suggestion_id(community.id)
  126 + suggestion.common_friends = common_friends.size
  127 + suggestion.save
  128 + end
  129 + end
  130 + end
  131 + end
  132 + end
  133 +
  134 + def self.communities_with_common_tags(person)
  135 + community_attempts = 0
  136 + tags = person.article_tags.keys
  137 + tags.each do |tag|
  138 + community_attempts += 1
  139 + return unless person.profile_suggestions.count < N_SUGGESTIONS && community_attempts < MAX_ATTEMPTS
  140 + tagged_content = ActsAsTaggableOn::Tagging.includes(:taggable).where(:tag_id => ActsAsTaggableOn::Tag.find_by_name(tag))
  141 + tagged_content.each do |tg|
  142 + profile = tg.taggable.profile
  143 + unless !profile.community? || person.is_member_of?(profile) || profile.already_request_membership?(person)
  144 + common_tags = tags & profile.article_tags.keys
  145 + if common_tags.size >= COMMON_TAGS
  146 + suggestion = person.profile_suggestions.find_or_initialize_by_suggestion_id(profile.id)
  147 + suggestion.common_tags = common_tags.size
  148 + suggestion.save
  149 + end
  150 + end
  151 + end
  152 + end
  153 + end
  154 +
  155 + def disable
  156 + self.enabled = false
  157 + self.save
  158 + end
  159 +
  160 +end
app/views/content_viewer/view_page.html.erb
@@ -80,8 +80,8 @@ @@ -80,8 +80,8 @@
80 </h3> 80 </h3>
81 <% end %> 81 <% end %>
82 82
83 - <% if @page.accept_comments? && @comments.present? && @comments.count > 1 %>  
84 - <%= link_to(_('Post a comment'), '#', :class => 'display-comment-form', :id => 'top-post-comment-button', :onclick => "jQuery('#page-comment-form .display-comment-form').first().click();") %> 83 + <% if @comments.present? && @comments.count > 1 %>
  84 + <%= link_to(_('Post a comment'), '#', :class => 'display-comment-form', :id => 'top-post-comment-button', :onclick => "jQuery('#page-comment-form .display-comment-form').first().click();") if @page.accept_comments? %>
85 85
86 <%= hidden_field_tag("page_url", url_for(:controller=>'content_viewer', :action=>'view_page', :profile=>profile.identifier, :page => @page.explode_path)) %> 86 <%= hidden_field_tag("page_url", url_for(:controller=>'content_viewer', :action=>'view_page', :profile=>profile.identifier, :page => @page.explode_path)) %>
87 <%= javascript_include_tag "comment_order.js" %> 87 <%= javascript_include_tag "comment_order.js" %>
@@ -90,12 +90,14 @@ @@ -90,12 +90,14 @@
90 <%= select_tag 'comment_order', options_for_select({_('Oldest first')=>'oldest', _('Newest first')=>'newest'}, @comment_order) %> 90 <%= select_tag 'comment_order', options_for_select({_('Oldest first')=>'oldest', _('Newest first')=>'newest'}, @comment_order) %>
91 <% end %> 91 <% end %>
92 </div> 92 </div>
  93 + <% end %>
93 94
94 - <ul class="article-comments-list"> 95 + <ul class="article-comments-list">
  96 + <% if @comments.present? %>
95 <%= render :partial => 'comment/comment', :collection => @comments %> 97 <%= render :partial => 'comment/comment', :collection => @comments %>
96 <%= pagination_links @comments, :param_name => 'comment_page' %> 98 <%= pagination_links @comments, :param_name => 'comment_page' %>
97 - </ul>  
98 - <% end %> 99 + <% end %>
  100 + </ul>
99 101
100 <% if @page.accept_comments? %> 102 <% if @page.accept_comments? %>
101 <div id='page-comment-form' class='page-comment-form'><%= render :partial => 'comment/comment_form', :locals =>{:url => {:controller => :comment, :action => :create}, :display_link => true, :cancel_triggers_hide => true}%></div> 103 <div id='page-comment-form' class='page-comment-form'><%= render :partial => 'comment/comment_form', :locals =>{:url => {:controller => :comment, :action => :create}, :display_link => true, :cancel_triggers_hide => true}%></div>
app/views/friends/index.html.erb
@@ -10,38 +10,8 @@ @@ -10,38 +10,8 @@
10 <%= link_to _('Do you want to see other people in this environment?'), :controller => 'search', :action => 'assets', :asset => 'people' %> 10 <%= link_to _('Do you want to see other people in this environment?'), :controller => 'search', :action => 'assets', :asset => 'people' %>
11 </em> 11 </em>
12 </p> 12 </p>
13 - <% else %>  
14 - <% button_bar do %>  
15 - <%= button(:back, _('Back to control panel'), :controller => 'profile_editor') %>  
16 - <%= button(:search, _('Find people'), :controller => 'search', :action => 'assets', :asset => 'people') %>  
17 - <% unless @plugins.dispatch(:remove_invite_friends_button).include?(true) %>  
18 - <%= button(:search, _('Invite people from my e-mail contacts'), :controller => 'invite', :action => 'invite_friends') %>  
19 - <% end %>  
20 - <% end %>  
21 <% end %> 13 <% end %>
22 14
23 - <ul class="profile-list">  
24 - <% @friends.each do |friend| %>  
25 - <li>  
26 - <%= link_to_profile profile_image(friend) + '<br/>' + friend.short_name,  
27 - friend.identifier, :class => 'profile-link' %>  
28 - <div class="controll">  
29 - <%= link_to content_tag('span',_('remove')),  
30 - { :action => 'remove', :id => friend.id },  
31 - :class => 'button icon-remove',  
32 - :title => _('remove') %>  
33 - <%= link_to content_tag('span',_('contact')),  
34 - friend.url.merge(:controller => 'contact', :action => 'new', :profile => friend.identifier),  
35 - :class => 'button icon-menu-mail',  
36 - :title => _('contact') %>  
37 - </div><!-- end class="controll" -->  
38 - </li>  
39 - <% end %>  
40 - </ul>  
41 - <div id='pagination-friends'>  
42 - <%= pagination_links @friends, :param_name => 'npage' %>  
43 - </div>  
44 -  
45 <% button_bar do %> 15 <% button_bar do %>
46 <%= button(:back, _('Back to control panel'), :controller => 'profile_editor') %> 16 <%= button(:back, _('Back to control panel'), :controller => 'profile_editor') %>
47 <%= button(:search, _('Find people'), :controller => 'search', :action => 'assets', :asset => 'people') %> 17 <%= button(:search, _('Find people'), :controller => 'search', :action => 'assets', :asset => 'people') %>
@@ -49,7 +19,23 @@ @@ -49,7 +19,23 @@
49 <%= button(:search, _('Invite people from my e-mail contacts'), :controller => 'invite', :action => 'invite_friends') %> 19 <%= button(:search, _('Invite people from my e-mail contacts'), :controller => 'invite', :action => 'invite_friends') %>
50 <% end %> 20 <% end %>
51 <% end %> 21 <% end %>
  22 +
  23 + <%= render :partial => 'shared/profile_list', :locals => { :profiles => @friends, :collection => :friends } %>
  24 + <div id='pagination-friends'>
  25 + <%= pagination_links @friends, :param_name => 'npage' %>
  26 + </div>
  27 +
  28 +<% end %>
  29 +
  30 +<% unless @suggestions.empty? %>
  31 + <br style="clear:both" />
  32 + <h2><%= _("Friends suggestions") %></h2>
  33 +
  34 + <%= render :partial => 'shared/profile_list', :locals => { :profiles => @suggestions, :collection => :friends_suggestions } %>
  35 +
  36 + <% button_bar do %>
  37 + <%= link_to _('See more suggestions...'), :action => 'suggest' %>
  38 + <% end %>
52 <% end %> 39 <% end %>
53 40
54 </div><!-- end id="manage_friends" --> 41 </div><!-- end id="manage_friends" -->
55 -  
app/views/friends/remove_suggestion.html.erb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +<div id="remove_suggestion">
  2 + <h1><%= _('Removing suggestion for friend: %s') % @person.name %></h1>
  3 +
  4 + <%= render :partial => 'shared/remove_suggestion', :locals => { :suggestion => @person } %>
  5 +</div>
app/views/friends/suggest.html.erb 0 → 100644
@@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
  1 +<div class='profiles-suggestions'>
  2 + <h1><%= _("Friends suggestions for %s") % profile.name %></h1>
  3 +
  4 + <% button_bar do %>
  5 + <%= button(:back, _('Go to friends list'), :controller => 'friends') %>
  6 + <% end %>
  7 +
  8 + <% if @suggestions.empty? %>
  9 + <p>
  10 + <em>
  11 + <%= _('You have no suggestions yet.') %>
  12 + <%= link_to _('Do you want to see other people in this environment?'), :controller => 'search', :action => 'assets', :asset => 'people' %>
  13 + </em>
  14 + </p>
  15 + <% else %>
  16 + <%= render :partial => 'shared/profile_list', :locals => { :profiles => @suggestions, :collection => :friends_suggestions } %>
  17 +
  18 + <%= pagination_links @suggestions, :param_name => 'npage' %>
  19 + <% end %>
  20 + <br style="clear:both" />
  21 +</div>
app/views/memberships/index.html.erb
@@ -8,13 +8,16 @@ @@ -8,13 +8,16 @@
8 <%= button :back, _('Go back'), :controller => 'profile_editor' %> 8 <%= button :back, _('Go back'), :controller => 'profile_editor' %>
9 <% end %> 9 <% end %>
10 10
11 -<% type_collection = [[nil, _('All')]] %> 11 +<% type_collection = [[0, _('All')]] %>
12 <% type_collection += @roles.sort_by {|role| role.id}.map{|r| ["#{r.id}", r.name]} %> 12 <% type_collection += @roles.sort_by {|role| role.id}.map{|r| ["#{r.id}", r.name]} %>
13 13
  14 +<%= javascript_include_tag "memberships_filter.js" %>
14 <p> 15 <p>
15 - <%= labelled_select(_('Filter')+': ', :filter_type, :first, :last, @filter, type_collection, :onchange => 'document.location.href = "?filter_type="+this.value')%> 16 + <%= labelled_select(_('Filter')+': ', :filter_type, :first, :last, @filter, type_collection, :id => 'memberships_filter')%>
16 </p> 17 </p>
17 18
  19 +<p><%= link_to _('See some suggestions of communities...'), :action => 'suggest' %></p>
  20 +
18 <% if @memberships.empty? %> 21 <% if @memberships.empty? %>
19 <p> 22 <p>
20 <em><%= _('No groups to list') %></em> 23 <em><%= _('No groups to list') %></em>
app/views/memberships/remove_suggestion.html.erb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +<div id="remove_suggestion">
  2 + <h1><%= _('Removing suggestion for community: %s') % @community.name %></h1>
  3 +
  4 + <%= render :partial => 'shared/remove_suggestion', :locals => { :suggestion => @community } %>
  5 +</div>
app/views/memberships/suggest.html.erb 0 → 100644
@@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
  1 +<div class='profiles-suggestions'>
  2 + <h1><%= _("Communities suggestions for %s") % profile.name %></h1>
  3 +
  4 + <% button_bar do %>
  5 + <%= button(:back, _('Go to groups list'), :controller => 'memberships') %>
  6 + <% end %>
  7 +
  8 + <% if @suggestions.empty? %>
  9 + <p>
  10 + <em>
  11 + <%= _('You have no suggestions yet.') %>
  12 + <%= link_to _('Do you want to see communities in this environment?'), :controller => 'search', :action => 'assets', :asset => 'communities' %>
  13 + </em>
  14 + </p>
  15 + <% else %>
  16 + <%= render :partial => 'shared/profile_list', :locals => { :profiles => @suggestions, :collection => :communities_suggestions } %>
  17 + <% end %>
  18 +
  19 + <%= pagination_links @suggestions, :param_name => 'npage' %>
  20 + <br style="clear:both" />
  21 +</div>
app/views/shared/_profile_list.html.erb 0 → 100644
@@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
  1 +<ul class="profile-list">
  2 + <% profiles.each do |profile| %>
  3 + <li>
  4 + <%= link_to_profile profile_image(profile) + '<br/>' + profile.short_name,
  5 + profile.identifier, :class => 'profile-link' %>
  6 + <div class="controll">
  7 + <% if collection == :friends %>
  8 + <%= button_without_text :remove, content_tag('span',_('remove')),
  9 + { :action => 'remove', :id => profile.id },
  10 + :title => _('remove') %>
  11 + <%= button_without_text 'menu-mail', content_tag('span',_('contact')),
  12 + profile.url.merge(:controller => 'contact', :action => 'new', :profile => profile.identifier),
  13 + :title => _('contact') %>
  14 + <% elsif collection == :friends_suggestions %>
  15 + <%= button_without_text :add, content_tag('span',_('add')),
  16 + profile.add_url,
  17 + :class => 'add-friend',
  18 + :title => _('Add friend') %>
  19 + <%= button_without_text :remove, content_tag('span',_('remove')),
  20 + { :action => 'remove_suggestion', :id => profile.identifier },
  21 + :title => _('Remove suggestion'),
  22 + :method => 'post',
  23 + :confirm => _('Are you sure you want to remove this suggestion?') %>
  24 + <% elsif collection == :communities_suggestions %>
  25 + <%= button_without_text :add, content_tag('span',_('join')),
  26 + profile.join_url,
  27 + :class => 'join-community',
  28 + :title => _("Join %s") % profile.name %>
  29 + <%= button_without_text :remove, content_tag('span',_('remove')),
  30 + { :action => 'remove_suggestion', :id => profile.identifier },
  31 + :title => _('Remove suggestion'),
  32 + :method => 'post',
  33 + :confirm => _('Are you sure you want to remove this suggestion?') %>
  34 + <% end %>
  35 + </div><!-- end class="controll" -->
  36 + </li>
  37 + <% end %>
  38 +</ul>
app/views/shared/_remove_suggestion.html.erb 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +<%= profile_image suggestion, :thumb, :class => 'suggestion_picture' %>
  2 +
  3 +<p>
  4 +<%= _('Are you sure you want to remove %s from your suggestions list?') % suggestion.name %>
  5 +</p>
  6 +
  7 +<%= form_tag do %>
  8 + <%= submit_button(:ok, _("Yes, I want to remove %s") % suggestion.name) %>
  9 + <%= button(:cancel, _("No"), :action => 'suggest') %>
  10 +<% end %>
app/views/user_mailer/profiles_suggestions_email.html.erb 0 → 100644
@@ -0,0 +1,32 @@ @@ -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 %>
db/migrate/20140720144212_create_profile_suggestions.rb 0 → 100644
@@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
  1 +class CreateProfileSuggestions < ActiveRecord::Migration
  2 + def change
  3 + create_table :profile_suggestions do |t|
  4 + t.references :person
  5 + t.references :suggestion
  6 + t.string :suggestion_type
  7 + t.text :categories
  8 + t.boolean :enabled, :default => true
  9 +
  10 + t.timestamps
  11 + end
  12 + add_index :profile_suggestions, :person_id
  13 + add_index :profile_suggestions, :suggestion_id
  14 + end
  15 +end
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 # 11 #
12 # It's strongly recommended to check this file into your version control system. 12 # It's strongly recommended to check this file into your version control system.
13 13
14 -ActiveRecord::Schema.define(:version => 20140605222753) do 14 +ActiveRecord::Schema.define(:version => 20140720144212) do
15 15
16 create_table "abuse_reports", :force => true do |t| 16 create_table "abuse_reports", :force => true do |t|
17 t.integer "reporter_id" 17 t.integer "reporter_id"
@@ -446,6 +446,19 @@ ActiveRecord::Schema.define(:version =&gt; 20140605222753) do @@ -446,6 +446,19 @@ ActiveRecord::Schema.define(:version =&gt; 20140605222753) do
446 add_index "products", ["product_category_id"], :name => "index_products_on_product_category_id" 446 add_index "products", ["product_category_id"], :name => "index_products_on_product_category_id"
447 add_index "products", ["profile_id"], :name => "index_products_on_profile_id" 447 add_index "products", ["profile_id"], :name => "index_products_on_profile_id"
448 448
  449 + create_table "profile_suggestions", :force => true do |t|
  450 + t.integer "person_id"
  451 + t.integer "suggestion_id"
  452 + t.string "suggestion_type"
  453 + t.text "categories"
  454 + t.boolean "enabled", :default => true
  455 + t.datetime "created_at", :null => false
  456 + t.datetime "updated_at", :null => false
  457 + end
  458 +
  459 + add_index "profile_suggestions", ["person_id"], :name => "index_profile_suggestions_on_person_id"
  460 + add_index "profile_suggestions", ["suggestion_id"], :name => "index_profile_suggestions_on_suggestion_id"
  461 +
449 create_table "profiles", :force => true do |t| 462 create_table "profiles", :force => true do |t|
450 t.string "name" 463 t.string "name"
451 t.string "type" 464 t.string "type"
etc/pound.cfg 0 → 100644
@@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
  1 +LogLevel 1
  2 +Alive 10
  3 +Client 120
  4 +TimeOut 300
  5 +Control "/var/run/pound/poundctl.socket"
  6 +
  7 +ListenHTTP
  8 + Address 192.168.1.86
  9 + Port 80
  10 + xHTTP 1
  11 + # uncomment code above if you are using chat
  12 + #Service
  13 + # URL "/http-bind.*"
  14 + # Backend
  15 + # Address 127.0.0.1
  16 + # Port 5280
  17 + # End
  18 + #End
  19 + Service
  20 + Redirect "https://mydomain.example.com"
  21 + End
  22 +End
  23 +
  24 +ListenHTTPS
  25 + Address 192.168.1.86
  26 + Port 443
  27 + Cert "/etc/noosfero/ssl/noosfero.pem"
  28 + Ciphers "ECDHE-RSA-AES256-SHA384:AES256-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM"
  29 + xHTTP 1
  30 + # uncomment code above if you are using chat
  31 + #Service
  32 + # URL "/http-bind.*"
  33 + # Backend
  34 + # Address 127.0.0.1
  35 + # Port 5280
  36 + # End
  37 + #End
  38 + Service
  39 + BackEnd
  40 + Address 127.0.0.1
  41 + Port 6081
  42 + End
  43 + End
  44 +End
lib/profile_suggestions_job.rb 0 → 100644
@@ -0,0 +1,17 @@ @@ -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
plugins/people_block/controllers/people_block_plugin_profile_controller.rb
@@ -4,7 +4,7 @@ class PeopleBlockPluginProfileController &lt; ProfileController @@ -4,7 +4,7 @@ class PeopleBlockPluginProfileController &lt; ProfileController
4 4
5 def members 5 def members
6 if is_cache_expired?(profile.members_cache_key(params)) 6 if is_cache_expired?(profile.members_cache_key(params))
7 - if(params[:role_key]) 7 + unless params[:role_key].blank?
8 role = Role.find_by_key_and_environment_id(params[:role_key], profile.environment) 8 role = Role.find_by_key_and_environment_id(params[:role_key], profile.environment)
9 @members = profile.members.with_role(role.id).includes(relations_to_include).paginate(:per_page => members_per_page, :page => params[:npage]) 9 @members = profile.members.with_role(role.id).includes(relations_to_include).paginate(:per_page => members_per_page, :page => params[:npage])
10 @members_title = role.name 10 @members_title = role.name
plugins/people_block/test/functional/people_block_plugin_profile_controller_test.rb
@@ -34,7 +34,7 @@ class PeopleBlockPluginProfileControllerTest &lt; ActionController::TestCase @@ -34,7 +34,7 @@ class PeopleBlockPluginProfileControllerTest &lt; ActionController::TestCase
34 attr_accessor :profile, :block, :admin, :member, :moderator 34 attr_accessor :profile, :block, :admin, :member, :moderator
35 35
36 should 'list members without role_key' do 36 should 'list members without role_key' do
37 - get :members, :profile => profile.identifier 37 + get :members, :profile => profile.identifier, :role_key => ""
38 assert_response :success 38 assert_response :success
39 assert_template 'members' 39 assert_template 'members'
40 assert_equivalent [@admin, @member, @moderator], assigns(:members) 40 assert_equivalent [@admin, @member, @moderator], assigns(:members)
plugins/stoa/lib/ext/profile_suggestion.rb 0 → 100644
@@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
  1 +require_dependency 'profile_suggestion'
  2 +
  3 +class ProfileSuggestion
  4 +
  5 + CATEGORIES.merge!({:common_classroom => _('Classroom in common')})
  6 +
  7 + RULES += %w[
  8 + people_with_common_classroom
  9 + ]
  10 +
  11 + def self.people_with_common_classroom(person)
  12 + usp_id = person.usp_id
  13 + return if usp_id.nil?
  14 + person_attempts = 0
  15 + StoaPlugin::UspAlunoTurmaGrad.classrooms_from_person(usp_id).each do |classroom|
  16 + person_attempts += 1
  17 + return unless person.profile_suggestions.count < N_SUGGESTIONS && person_attempts < MAX_ATTEMPTS
  18 + StoaPlugin::UspAlunoTurmaGrad.find_all_by_codtur(classroom.codtur).each do |same_class|
  19 + classmate = Person.find_by_usp_id(same_class.codpes)
  20 + unless classmate.nil? || classmate == person || classmate.is_a_friend?(person) || person.already_request_friendship?(classmate)
  21 + suggestion = person.profile_suggestions.find_or_initialize_by_suggestion_id(classmate.id)
  22 + suggestion.common_classroom = 1
  23 + suggestion.save
  24 + end
  25 + end
  26 + end
  27 + end
  28 +
  29 + def self.people_with_common_discipline(person)
  30 + person_attempts = 0
  31 + person_attempts += 1
  32 + return unless person.profile_suggestions.count < N_SUGGESTIONS && person_attempts < MAX_ATTEMPTS
  33 + end
  34 +
  35 +end
plugins/stoa/lib/stoa_plugin/usp_aluno_turma_grad.rb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +class StoaPlugin::UspAlunoTurmaGrad < ActiveRecord::Base
  2 +
  3 + establish_connection(:stoa)
  4 + set_table_name('alunoturma_gr')
  5 +
  6 + def self.exists?(usp_id)
  7 + StoaPlugin::UspUser.find_by_codpes(usp_id.to_i)
  8 + end
  9 +
  10 + def self.classrooms_from_person(usp_id)
  11 + StoaPlugin::UspAlunoTurmaGrad.find_all_by_codpes(usp_id)
  12 + end
  13 +
  14 +end
public/javascripts/add-and-join.js
@@ -12,7 +12,7 @@ jQuery(function($) { @@ -12,7 +12,7 @@ jQuery(function($) {
12 }) 12 })
13 13
14 $(".join-community").live('click', function(){ 14 $(".join-community").live('click', function(){
15 - clicked = $(".join-community"); 15 + clicked = $(this);
16 url = clicked.attr("href"); 16 url = clicked.attr("href");
17 loading_for_button(this); 17 loading_for_button(this);
18 $.post(url, function(data){ 18 $.post(url, function(data){
public/javascripts/chat.js
@@ -99,7 +99,7 @@ jQuery(function($) { @@ -99,7 +99,7 @@ jQuery(function($) {
99 }, 99 },
100 100
101 render_body_message: function(body) { 101 render_body_message: function(body) {
102 - body = body.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\r?\n/g, '<br>'); 102 + body = body.replace(/\r?\n/g, '<br>');
103 body = $().emoticon(body); 103 body = $().emoticon(body);
104 body = linkify(body, { 104 body = linkify(body, {
105 callback: function(text, href) { 105 callback: function(text, href) {
@@ -320,7 +320,7 @@ jQuery(function($) { @@ -320,7 +320,7 @@ jQuery(function($) {
320 var jid_id = Jabber.jid_to_id(jid); 320 var jid_id = Jabber.jid_to_id(jid);
321 var name = Jabber.name_of(jid_id); 321 var name = Jabber.name_of(jid_id);
322 create_conversation_tab(name, jid_id); 322 create_conversation_tab(name, jid_id);
323 - Jabber.show_message(jid, name, message.body, 'other', Strophe.getNodeFromJid(jid)); 323 + Jabber.show_message(jid, name, escape_html(message.body), 'other', Strophe.getNodeFromJid(jid));
324 $.sound.play('/sounds/receive.wav'); 324 $.sound.play('/sounds/receive.wav');
325 return true; 325 return true;
326 }, 326 },
@@ -336,7 +336,7 @@ jQuery(function($) { @@ -336,7 +336,7 @@ jQuery(function($) {
336 // is a message from another user, not mine 336 // is a message from another user, not mine
337 else if ($own_name != name) { 337 else if ($own_name != name) {
338 var jid = Jabber.rooms[Jabber.jid_to_id(message.from)][name]; 338 var jid = Jabber.rooms[Jabber.jid_to_id(message.from)][name];
339 - Jabber.show_message(message.from, name, message.body, name, Strophe.getNodeFromJid(jid)); 339 + Jabber.show_message(message.from, name, escape_html(message.body), name, Strophe.getNodeFromJid(jid));
340 $.sound.play('/sounds/receive.wav'); 340 $.sound.play('/sounds/receive.wav');
341 } 341 }
342 return true; 342 return true;
@@ -432,7 +432,7 @@ jQuery(function($) { @@ -432,7 +432,7 @@ jQuery(function($) {
432 .c('body').t(body).up() 432 .c('body').t(body).up()
433 .c('active', {xmlns: Strophe.NS.CHAT_STATES}); 433 .c('active', {xmlns: Strophe.NS.CHAT_STATES});
434 Jabber.connection.send(message); 434 Jabber.connection.send(message);
435 - Jabber.show_message(jid, $own_name, body, 'self', Strophe.getNodeFromJid(Jabber.connection.jid)); 435 + Jabber.show_message(jid, $own_name, escape_html(body), 'self', Strophe.getNodeFromJid(Jabber.connection.jid));
436 }, 436 },
437 437
438 is_a_room: function(jid_id) { 438 is_a_room: function(jid_id) {
@@ -632,6 +632,13 @@ jQuery(function($) { @@ -632,6 +632,13 @@ jQuery(function($) {
632 } 632 }
633 } 633 }
634 634
  635 + function escape_html(body) {
  636 + return body
  637 + .replace(/&/g, '&amp;')
  638 + .replace(/</g, '&lt;')
  639 + .replace(/>/g, '&gt;');
  640 + }
  641 +
635 }); 642 });
636 643
637 function checkTime(i) { 644 function checkTime(i) {
public/javascripts/memberships_filter.js 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +jQuery(document).ready(function($){
  2 + $("#memberships_filter").change(function(){
  3 + var filter = $(this).find("option:selected").val();
  4 + redirect_to('?filter_type=' + filter);
  5 + });
  6 +});
public/stylesheets/application.css
@@ -3869,12 +3869,16 @@ h1#agenda-title { @@ -3869,12 +3869,16 @@ h1#agenda-title {
3869 3869
3870 /* ==> @import url(manage_contacts_list.css); <== */ 3870 /* ==> @import url(manage_contacts_list.css); <== */
3871 3871
3872 -.controller-favorite_enterprises .profile-list, .controller-friends .profile-list { 3872 +.controller-favorite_enterprises .profile-list,
  3873 +.controller-friends .profile-list,
  3874 +.profiles-suggestions .profile-list {
3873 margin: 0px; 3875 margin: 0px;
3874 padding: 0px; 3876 padding: 0px;
3875 list-style: none; 3877 list-style: none;
3876 } 3878 }
3877 -.controller-favorite_enterprises .profile-list li, .controller-friends .profile-list li { 3879 +.controller-favorite_enterprises .profile-list li,
  3880 +.controller-friends .profile-list li,
  3881 +.profiles-suggestions .profile-list li {
3878 float: left; 3882 float: left;
3879 width: 90px; 3883 width: 90px;
3880 height: 90px; 3884 height: 90px;
@@ -3886,52 +3890,74 @@ h1#agenda-title { @@ -3886,52 +3890,74 @@ h1#agenda-title {
3886 list-style: none; 3890 list-style: none;
3887 position: relative; 3891 position: relative;
3888 } 3892 }
3889 -.controller-favorite_enterprises .profile-list li:hover, .controller-friends .profile-list li:hover { 3893 +.controller-favorite_enterprises .profile-list li:hover,
  3894 +.controller-friends .profile-list li:hover,
  3895 +.profiles-suggestions .profile-list li:hover {
3890 border: 2px solid #eeeeec; 3896 border: 2px solid #eeeeec;
3891 } 3897 }
3892 -.controller-favorite_enterprises .profile-list img, .controller-friends .profile-list img { 3898 +.controller-favorite_enterprises .profile-list img,
  3899 +.controller-friends .profile-list img,
  3900 +.profiles-suggestions .profile-list img {
3893 border: none; 3901 border: none;
3894 } 3902 }
3895 -.controller-favorite_enterprises .profile-list a.profile-link, .controller-friends .profile-list a.profile-link { 3903 +.controller-favorite_enterprises .profile-list a.profile-link,
  3904 +.controller-friends .profile-list a.profile-link,
  3905 +.profiles-suggestions .profile-list a.profile-link {
3896 text-decoration: none; 3906 text-decoration: none;
3897 text-align: center; 3907 text-align: center;
3898 display: block; 3908 display: block;
3899 font-size: 11px; 3909 font-size: 11px;
3900 } 3910 }
3901 -.controller-favorite_enterprises .profile-list a.profile-link:hover, .controller-friends .profile-list a.profile-link:hover { 3911 +.controller-favorite_enterprises .profile-list a.profile-link:hover,
  3912 +.controller-friends .profile-list a.profile-link:hover,
  3913 +.profiles-suggestions .profile-list a.profile-link:hover {
3902 color: #FFF; 3914 color: #FFF;
3903 } 3915 }
3904 -.controller-favorite_enterprises .profile-list .profile_link span, .controller-friends .profile-list .profile_link span { 3916 +.controller-favorite_enterprises .profile-list .profile_link span,
  3917 +.controller-friends .profile-list .profile_link span,
  3918 +.profiles-suggestions .profile-list .profile_link span {
3905 width: 80px; 3919 width: 80px;
3906 display: block; 3920 display: block;
3907 overflow: hidden; 3921 overflow: hidden;
3908 } 3922 }
3909 -.controller-favorite_enterprises .profile-list, .controller-friends .profile-list { 3923 +.controller-favorite_enterprises .profile-list,
  3924 +.controller-friends .profile-list,
  3925 +.profiles-suggestions .profile-list {
3910 position: relative; 3926 position: relative;
3911 } 3927 }
3912 -.controller-favorite_enterprises .profile-list .controll, .controller-friends .profile-list .controll { 3928 +.controller-favorite_enterprises .profile-list .controll,
  3929 +.controller-friends .profile-list .controll,
  3930 +.profiles-suggestions .profile-list .controll {
3913 position: absolute; 3931 position: absolute;
3914 top: 7px; 3932 top: 7px;
3915 right: -10px; 3933 right: -10px;
3916 } 3934 }
3917 -.controller-favorite_enterprises .profile-list .controll a, .controller-friends .profile-list .controll a { 3935 +.controller-favorite_enterprises .profile-list .controll a,
  3936 +.controller-friends .profile-list .controll a,
  3937 +.profiles-suggestions .profile-list .controll a {
3918 display: block; 3938 display: block;
3919 margin-bottom: 2px; 3939 margin-bottom: 2px;
3920 } 3940 }
3921 -.controller-favorite_enterprises .msie6 .profile-list .controll a, .controller-friends .msie6 .profile-list .controll a { 3941 +.controller-favorite_enterprises .msie6 .profile-list .controll a,
  3942 +.controller-friends .msie6 .profile-list .controll a,
  3943 +.profiles-suggestions .msie6 .profile-list .controll a {
3922 width: 0px; 3944 width: 0px;
3923 } 3945 }
3924 -.controller-favorite_enterprises .button-bar, .controller-friends .button-bar { 3946 +.controller-favorite_enterprises .button-bar,
  3947 +.controller-friends .button-bar,
  3948 +.profiles-suggestions .button-bar {
3925 clear: both; 3949 clear: both;
3926 padding-top: 20px; 3950 padding-top: 20px;
3927 } 3951 }
3928 /* ==> public/stylesheets/controller_friends.css <== */ 3952 /* ==> public/stylesheets/controller_friends.css <== */
3929 3953
3930 -.controller-friends #remove_friend .friend_picture { 3954 +.controller-friends #remove_friend .friend_picture,
  3955 +#remove_suggestion .suggestion_picture {
3931 float: left; 3956 float: left;
3932 margin-right: 15px; 3957 margin-right: 15px;
3933 } 3958 }
3934 -.controller-friends #remove_friend form { 3959 +.controller-friends #remove_friend form,
  3960 +#remove_suggestion form {
3935 clear: both; 3961 clear: both;
3936 padding-top: 20px; 3962 padding-top: 20px;
3937 } 3963 }
test/functional/friends_controller_test.rb
@@ -70,4 +70,41 @@ class FriendsControllerTest &lt; ActionController::TestCase @@ -70,4 +70,41 @@ class FriendsControllerTest &lt; ActionController::TestCase
70 assert_no_tag :tag => 'a', :attributes => { :href => "/profile/testuser/invite/friends" } 70 assert_no_tag :tag => 'a', :attributes => { :href => "/profile/testuser/invite/friends" }
71 end 71 end
72 72
  73 + should 'not display list suggestions button if there is no suggestion' do
  74 + get :index, :profile => 'testuser'
  75 + assert_no_tag :tag => 'a', :content => 'Suggest friends', :attributes => { :href => "/myprofile/testuser/friends/suggest" }
  76 + end
  77 +
  78 + should 'display link to list suggestions page' do
  79 + profile.profile_suggestions.create(:suggestion => friend)
  80 + get :index, :profile => 'testuser'
  81 + assert_tag :tag => 'a', :content => 'See more suggestions...', :attributes => { :href => "/myprofile/testuser/friends/suggest" }
  82 + end
  83 +
  84 + should 'display people suggestions' do
  85 + profile.profile_suggestions.create(:suggestion => friend)
  86 + get :suggest, :profile => 'testuser'
  87 + assert_tag :tag => 'a', :content => friend.name, :attributes => { :href => "/profile/#{friend.identifier}" }
  88 + end
  89 +
  90 + should 'display button to add friend suggestion' do
  91 + profile.profile_suggestions.create(:suggestion => friend)
  92 + get :suggest, :profile => 'testuser'
  93 + assert_tag :tag => 'a', :attributes => { :href => "/profile/#{friend.identifier}/add" }
  94 + end
  95 +
  96 + should 'display button to remove people suggestion' do
  97 + profile.profile_suggestions.create(:suggestion => friend)
  98 + get :suggest, :profile => 'testuser'
  99 + assert_tag :tag => 'a', :attributes => { :href => "/myprofile/testuser/friends/remove_suggestion/#{friend.identifier}" }
  100 + end
  101 +
  102 + should 'remove suggestion of friend' do
  103 + suggestion = profile.profile_suggestions.create(:suggestion => friend)
  104 + post :remove_suggestion, :profile => 'testuser', :id => friend.identifier
  105 +
  106 + assert_redirected_to :action => 'suggest'
  107 + assert_equal false, ProfileSuggestion.find(suggestion.id).enabled
  108 + end
  109 +
73 end 110 end
test/functional/memberships_controller_test.rb
@@ -328,4 +328,42 @@ class MembershipsControllerTest &lt; ActionController::TestCase @@ -328,4 +328,42 @@ class MembershipsControllerTest &lt; ActionController::TestCase
328 assert_includes assigns(:roles), role1 328 assert_includes assigns(:roles), role1
329 assert_not_includes assigns(:roles), role2 329 assert_not_includes assigns(:roles), role2
330 end 330 end
  331 +
  332 + should 'display list suggestions button' do
  333 + community = fast_create(Community)
  334 + profile.profile_suggestions.create(:suggestion => community)
  335 + get :index, :profile => 'testuser'
  336 + assert_tag :tag => 'a', :content => 'See some suggestions of communities...', :attributes => { :href => "/myprofile/testuser/memberships/suggest" }
  337 + end
  338 +
  339 + should 'display communities suggestions' do
  340 + community = fast_create(Community)
  341 + profile.profile_suggestions.create(:suggestion => community)
  342 + get :suggest, :profile => 'testuser'
  343 + assert_tag :tag => 'a', :content => community.name, :attributes => { :href => "/profile/#{community.identifier}" }
  344 + end
  345 +
  346 + should 'display button to join on community suggestion' do
  347 + community = fast_create(Community)
  348 + profile.profile_suggestions.create(:suggestion => community)
  349 + get :suggest, :profile => 'testuser'
  350 + assert_tag :tag => 'a', :attributes => { :href => "/profile/#{community.identifier}/join" }
  351 + end
  352 +
  353 + should 'display button to remove community suggestion' do
  354 + community = fast_create(Community)
  355 + profile.profile_suggestions.create(:suggestion => community)
  356 + get :suggest, :profile => 'testuser'
  357 + assert_tag :tag => 'a', :attributes => { :href => "/myprofile/testuser/memberships/remove_suggestion/#{community.identifier}" }
  358 + end
  359 +
  360 + should 'remove suggestion of community' do
  361 + community = fast_create(Community)
  362 + suggestion = profile.profile_suggestions.create(:suggestion => community)
  363 + post :remove_suggestion, :profile => 'testuser', :id => community.identifier
  364 +
  365 + assert_redirected_to :action => 'suggest'
  366 + assert_equal false, ProfileSuggestion.find(suggestion.id).enabled
  367 + end
  368 +
331 end 369 end
test/unit/add_friend_test.rb
@@ -137,4 +137,11 @@ class AddFriendTest &lt; ActiveSupport::TestCase @@ -137,4 +137,11 @@ class AddFriendTest &lt; ActiveSupport::TestCase
137 assert_match(/#{task.requestor.name} wants to be your friend/, email.subject) 137 assert_match(/#{task.requestor.name} wants to be your friend/, email.subject)
138 end 138 end
139 139
  140 + should 'disable suggestion if profile requested friendship' do
  141 + suggestion = ProfileSuggestion.create(:person => person1, :suggestion => person2, :enabled => true)
  142 +
  143 + task = AddFriend.create(:person => person1, :friend => person2)
  144 + assert_equal false, ProfileSuggestion.find(suggestion.id).enabled
  145 + end
  146 +
140 end 147 end
test/unit/person_test.rb
@@ -1481,4 +1481,79 @@ class PersonTest &lt; ActiveSupport::TestCase @@ -1481,4 +1481,79 @@ class PersonTest &lt; ActiveSupport::TestCase
1481 person.reload 1481 person.reload
1482 end 1482 end
1483 end 1483 end
  1484 +
  1485 + should 'have a list of suggested people to be friend' do
  1486 + person = create_user('person').person
  1487 + suggested_friend = fast_create(Person)
  1488 +
  1489 + ProfileSuggestion.create(:person => person, :suggestion => suggested_friend)
  1490 + assert_equal [suggested_friend], person.suggested_people
  1491 + end
  1492 +
  1493 + should 'have a list of suggested communities to be member' do
  1494 + person = create_user('person').person
  1495 + suggested_community = fast_create(Community)
  1496 +
  1497 + ProfileSuggestion.create(:person => person, :suggestion => suggested_community)
  1498 + assert_equal [suggested_community], person.suggested_communities
  1499 + end
  1500 +
  1501 + should 'remove profile suggestion when person is destroyed' do
  1502 + person = create_user('person').person
  1503 + suggested_community = fast_create(Community)
  1504 +
  1505 + suggestion = ProfileSuggestion.create(:person => person, :suggestion => suggested_community)
  1506 +
  1507 + person.destroy
  1508 + assert_raise ActiveRecord::RecordNotFound do
  1509 + ProfileSuggestion.find suggestion.id
  1510 + end
  1511 + end
  1512 +
  1513 + should 'remove profile suggestion when suggested profile is destroyed' do
  1514 + person = create_user('person').person
  1515 + suggested_community = fast_create(Community)
  1516 +
  1517 + suggestion = ProfileSuggestion.create(:person => person, :suggestion => suggested_community)
  1518 +
  1519 + suggested_community.destroy
  1520 + assert_raise ActiveRecord::RecordNotFound do
  1521 + ProfileSuggestion.find suggestion.id
  1522 + end
  1523 + end
  1524 +
  1525 + should 'not suggest disabled suggestion of people' do
  1526 + person = create_user('person').person
  1527 + suggested_person = fast_create(Person)
  1528 + disabled_suggested_person = fast_create(Person)
  1529 +
  1530 + enabled_suggestion = ProfileSuggestion.create(:person => person, :suggestion => suggested_person)
  1531 + disabled_suggestion = ProfileSuggestion.create(:person => person, :suggestion => disabled_suggested_person, :enabled => false)
  1532 +
  1533 + assert_equal [suggested_person], person.suggested_people
  1534 + end
  1535 +
  1536 + should 'not suggest disabled suggestion of communities' do
  1537 + person = create_user('person').person
  1538 + suggested_community = fast_create(Community)
  1539 + disabled_suggested_community = fast_create(Community)
  1540 +
  1541 + enabled_suggestion = ProfileSuggestion.create(:person => person, :suggestion => suggested_community)
  1542 + disabled_suggestion = ProfileSuggestion.create(:person => person, :suggestion => disabled_suggested_community, :enabled => false)
  1543 +
  1544 + assert_equal [suggested_community], person.suggested_communities
  1545 + end
  1546 +
  1547 + should 'return url to people suggestions for a person' do
  1548 + environment = create_environment('mycolivre.net')
  1549 + profile = build(Person, :identifier => 'testprofile', :environment_id => create_environment('mycolivre.net').id)
  1550 + assert_equal({ :host => "mycolivre.net", :profile => 'testprofile', :controller => 'friends', :action => 'suggest' }, profile.people_suggestions_url)
  1551 + end
  1552 +
  1553 + should 'return url to communities suggestions for a person' do
  1554 + environment = create_environment('mycolivre.net')
  1555 + profile = build(Person, :identifier => 'testprofile', :environment_id => create_environment('mycolivre.net').id)
  1556 + assert_equal({ :host => "mycolivre.net", :profile => 'testprofile', :controller => 'memberships', :action => 'suggest' }, profile.communities_suggestions_url)
  1557 + end
  1558 +
1484 end 1559 end
test/unit/profile_suggestion_test.rb 0 → 100644
@@ -0,0 +1,121 @@ @@ -0,0 +1,121 @@
  1 +# encoding: UTF-8
  2 +require File.dirname(__FILE__) + '/../test_helper'
  3 +
  4 +class ProfileSuggestionTest < ActiveSupport::TestCase
  5 +
  6 + def setup
  7 + @person = create_user('test_user').person
  8 + @community = fast_create(Community)
  9 + end
  10 + attr_reader :person, :community
  11 +
  12 + should 'save the profile class' do
  13 + suggestion = ProfileSuggestion.create(:person => person, :suggestion => community)
  14 + assert_equal 'Community', suggestion.suggestion_type
  15 + end
  16 +
  17 + should 'only accept pre-defined categories' do
  18 + suggestion = ProfileSuggestion.new(:person => person, :suggestion => community)
  19 +
  20 + suggestion.categories = {:unexistent => 1}
  21 + suggestion.valid?
  22 + assert suggestion.errors[:categories.to_s].present?
  23 + end
  24 +
  25 + should 'disable a suggestion' do
  26 + suggestion = ProfileSuggestion.create(:person => person, :suggestion => community)
  27 +
  28 + suggestion.disable
  29 + assert_equal false, ProfileSuggestion.find(suggestion.id).enabled?
  30 + end
  31 +
  32 + should 'not suggest the same community twice' do
  33 + ProfileSuggestion.create(:person => person, :suggestion => community)
  34 +
  35 + repeated_suggestion = ProfileSuggestion.new(:person => person, :suggestion => community)
  36 +
  37 + repeated_suggestion.valid?
  38 + assert_equal true, repeated_suggestion.errors[:suggestion_id.to_s].present?
  39 + end
  40 +
  41 + should 'update existing person suggestion when the number of common friends increase' do
  42 + suggested_person = create_user('test_user').person
  43 + suggestion = ProfileSuggestion.create(:person => person, :suggestion => suggested_person, :common_friends => 2)
  44 +
  45 + friend = create_user('friend').person
  46 + friend2 = create_user('friend2').person
  47 + friend3 = create_user('friend2').person
  48 + person.add_friend friend
  49 + person.add_friend friend2
  50 + person.add_friend friend3
  51 +
  52 + friend.add_friend suggested_person
  53 +
  54 + suggested_person.add_friend friend
  55 + suggested_person.add_friend friend2
  56 + suggested_person.add_friend friend3
  57 +
  58 + assert_difference 'ProfileSuggestion.find(suggestion.id).common_friends', 1 do
  59 + ProfileSuggestion.friends_of_friends(person)
  60 + end
  61 + end
  62 +
  63 + should 'update existing person suggestion when the number of common communities increase' do
  64 + suggested_person = create_user('test_user').person
  65 + suggestion = ProfileSuggestion.create(:person => person, :suggestion => suggested_person, :common_communities => 1)
  66 +
  67 + community.add_member person
  68 + community.add_member suggested_person
  69 +
  70 + community2 = fast_create(Community)
  71 + community2.add_member person
  72 + community2.add_member suggested_person
  73 +
  74 + assert_difference 'ProfileSuggestion.find(suggestion.id).common_communities', 1 do
  75 + ProfileSuggestion.people_with_common_communities(person)
  76 + end
  77 + end
  78 +
  79 + should 'update existing person suggestion when the number of common tags increase' do
  80 + suggested_person = create_user('test_user').person
  81 + suggestion = ProfileSuggestion.create(:person => person, :suggestion => suggested_person, :common_tags => 1)
  82 +
  83 + create(Article, :created_by => person, :profile => person, :tag_list => 'first-tag, second-tag, third-tag, fourth-tag')
  84 + create(Article, :created_by => suggested_person, :profile => suggested_person, :tag_list => 'first-tag, second-tag, third-tag')
  85 +
  86 + assert_difference 'ProfileSuggestion.find(suggestion.id).common_tags', 2 do
  87 + ProfileSuggestion.people_with_common_tags(person)
  88 + end
  89 + end
  90 +
  91 + should 'update existing community suggestion when the number of common friends increase' do
  92 + suggestion = ProfileSuggestion.create(:person => person, :suggestion => community, :common_friends => 1)
  93 +
  94 + member1 = create_user('member1').person
  95 + member2 = create_user('member2').person
  96 +
  97 + person.add_friend member1
  98 + person.add_friend member2
  99 +
  100 + community.add_member member1
  101 + community.add_member member2
  102 +
  103 + assert_difference 'ProfileSuggestion.find(suggestion.id).common_friends', 1 do
  104 + ProfileSuggestion.communities_with_common_friends(person)
  105 + end
  106 +
  107 + end
  108 +
  109 + should 'update existing community suggestion when the number of common tags increase' do
  110 + other_person = create_user('test_user').person
  111 +
  112 + suggestion = ProfileSuggestion.create(:person => person, :suggestion => community, :common_tags => 1)
  113 +
  114 + create(Article, :created_by => person, :profile => person, :tag_list => 'first-tag, second-tag, third-tag, fourth-tag')
  115 + create(Article, :created_by => other_person, :profile => community, :tag_list => 'first-tag, second-tag, third-tag')
  116 +
  117 + assert_difference 'ProfileSuggestion.find(suggestion.id).common_tags', 2 do
  118 + ProfileSuggestion.communities_with_common_tags(person)
  119 + end
  120 + end
  121 +end
test/unit/profile_suggestions_job_test.rb 0 → 100644
@@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
  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 +
  10 + person.add_friend friend
  11 + person.add_friend friend2
  12 +
  13 + friend_of_friend = create_user('friend_of_friend').person
  14 + friend.add_friend friend_of_friend
  15 +
  16 + friend_of_friend.add_friend friend
  17 + friend_of_friend.add_friend friend2
  18 +
  19 + job = ProfileSuggestionsJob.new(person.id)
  20 + assert_difference 'ProfileSuggestion.count', 1 do
  21 + job.perform
  22 + end
  23 + assert_equal [friend_of_friend], person.suggested_people
  24 + end
  25 +
  26 + should 'suggest friends from communities' do
  27 + person = create_user('person').person
  28 + community = fast_create(Community)
  29 +
  30 + member1 = create_user('member1').person
  31 + member2 = create_user('member2').person
  32 +
  33 + community.add_member person
  34 + community.add_member member1
  35 + community.add_member member2
  36 +
  37 + job = ProfileSuggestionsJob.new(person.id)
  38 + assert_difference 'ProfileSuggestion.count', 2 do
  39 + job.perform
  40 + end
  41 + assert_equivalent [member1, member2], person.suggested_people
  42 + end
  43 +
  44 + should 'suggest friends from tags' do
  45 + person = create_user('person').person
  46 + person2 = create_user('person2').person
  47 + person3 = create_user('person3').person
  48 +
  49 + create(Article, :created_by => person, :profile => person, :tag_list => 'first-tag, second-tag')
  50 + create(Article, :created_by => person2, :profile => person2, :tag_list => 'first-tag, second-tag, third-tag')
  51 + create(Article, :created_by => person3, :profile => person3, :tag_list => 'first-tag')
  52 +
  53 + job = ProfileSuggestionsJob.new(person.id)
  54 + assert_difference 'ProfileSuggestion.count', 1 do
  55 + job.perform
  56 + end
  57 + assert_equivalent [person2], person.suggested_people
  58 + end
  59 +
  60 + should 'suggest from communities friends' do
  61 + person = create_user('person').person
  62 +
  63 + member1 = create_user('member1').person
  64 + member2 = create_user('member2').person
  65 +
  66 + person.add_friend member1
  67 + person.add_friend member2
  68 +
  69 + community = fast_create(Community)
  70 + community.add_member member1
  71 + community.add_member member2
  72 +
  73 + job = ProfileSuggestionsJob.new(person.id)
  74 + assert_difference 'ProfileSuggestion.count', 1 do
  75 + job.perform
  76 + end
  77 + assert_equivalent [community], person.suggested_communities
  78 + end
  79 +
  80 + should 'suggest communities from tags' do
  81 + person = create_user('person').person
  82 + person2 = create_user('person2').person
  83 +
  84 + community = fast_create(Community)
  85 + community.add_admin person2
  86 +
  87 + create(Article, :created_by => person, :profile => person, :tag_list => 'first-tag, second-tag')
  88 + create(Article, :created_by => person2, :profile => community, :tag_list => 'first-tag, second-tag, third-tag')
  89 +
  90 + job = ProfileSuggestionsJob.new(person.id)
  91 + assert_difference 'ProfileSuggestion.count', 1 do
  92 + job.perform
  93 + end
  94 + assert_equivalent [community], person.suggested_communities
  95 + end
  96 +
  97 +end
test/unit/profile_test.rb
@@ -1987,4 +1987,14 @@ class ProfileTest &lt; ActiveSupport::TestCase @@ -1987,4 +1987,14 @@ class ProfileTest &lt; ActiveSupport::TestCase
1987 template.save! 1987 template.save!
1988 assert_equal body, template.welcome_page_content 1988 assert_equal body, template.welcome_page_content
1989 end 1989 end
  1990 +
  1991 + should 'disable suggestion if profile requested membership' do
  1992 + person = fast_create(Person)
  1993 + community = fast_create(Community)
  1994 + suggestion = ProfileSuggestion.create(:person => person, :suggestion => community, :enabled => true)
  1995 +
  1996 + community.add_member person
  1997 + assert_equal false, ProfileSuggestion.find(suggestion.id).enabled
  1998 + end
  1999 +
1990 end 2000 end
util/chat/ejabberd.cfg 0 → 100644
@@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
  1 +%%%
  2 +%%% Noosfero ejabberd configuration file
  3 +%%% This config must be in UTF-8 encoding
  4 +%%%
  5 +
  6 +{acl, admin, {user, "admin", "mydomain.example.com"}}.
  7 +{hosts, ["mydomain.example.com"]}.
  8 +
  9 +{loglevel, 4}.
  10 +{listen,
  11 + [
  12 + {5222, ejabberd_c2s, [
  13 + {access, c2s},
  14 + {shaper, c2s_shaper},
  15 + {max_stanza_size, 65536},
  16 + starttls, {certfile, "/etc/ejabberd/ejabberd.pem"}
  17 + ]},
  18 + {5280, ejabberd_http, [
  19 + http_bind,
  20 + http_poll
  21 + ]}
  22 + ]}.
  23 +{max_fsm_queue, 1000}.
  24 +{auth_method, odbc}.
  25 +{odbc_server, "DSN=PostgreSQLEjabberdNoosfero"}.
  26 +{shaper, normal, {maxrate, 10000000}}.
  27 +{shaper, fast, {maxrate, 50000}}.
  28 +{acl, local, {user_regexp, ""}}.
  29 +{access, max_user_sessions, [{10, all}]}.
  30 +{access, local, [{allow, local}]}.
  31 +{access, c2s, [{deny, blocked},
  32 + {allow, all}]}.
  33 +{access, c2s_shaper, [{none, admin},
  34 + {normal, all}]}.
  35 +{access, announce, [{allow, admin}]}.
  36 +{access, configure, [{allow, admin}]}.
  37 +{access, muc_admin, [{allow, admin}]}.
  38 +{access, muc, [{allow, all}]}.
  39 +{access, pubsub_createnode, [{allow, all}]}.
  40 +{language, "pt"}.
  41 +{modules,
  42 + [
  43 + {mod_adhoc, []},
  44 + {mod_announce, [{access, announce}]}, % requires mod_adhoc
  45 + {mod_caps, []},
  46 + {mod_configure,[]}, % requires mod_adhoc
  47 + {mod_disco, []},
  48 + {mod_last, []},
  49 + {mod_muc, [
  50 + {access, muc},
  51 + {access_create, muc},
  52 + {access_persistent, muc},
  53 + {access_admin, muc_admin},
  54 + {max_users, 500},
  55 + {default_room_options, [{anonymous, false}]}
  56 + ]},
  57 + {mod_privacy_odbc, []},
  58 + {mod_private_odbc, []},
  59 + {mod_proxy65, [
  60 + {access, local},
  61 + {shaper, c2s_shaper}
  62 + ]},
  63 + {mod_roster_odbc, []},
  64 + {mod_stats, []},
  65 + {mod_time, []},
  66 + {mod_vcard, []},
  67 + {mod_http_bind, []},
  68 + {mod_version, []}
  69 + ]}.
  70 +
  71 +%%% Local Variables:
  72 +%%% mode: erlang
  73 +%%% End:
  74 +%%% vim: set filetype=erlang tabstop=8:
util/chat/odbc.ini 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +[PostgreSQLEjabberdNoosfero]
  2 +Description = PostgreSQL Noosfero ejabberd database
  3 +Driver = PostgreSQL Unicode
  4 +Trace = No
  5 +TraceFile = /tmp/psqlodbc.log
  6 +Database = noosfero
  7 +Servername = localhost
  8 +UserName = noosfero
  9 +Password = <copy the password present in the database.yml file>
  10 +Port =
  11 +ReadOnly = No
  12 +RowVersioning = No
  13 +ShowSystemTables = No
  14 +ShowOidColumn = No
  15 +FakeOidIndex = No
  16 +ConnSettings = SET search_path TO ejabberd
util/chat/odbcinst.ini 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +[PostgreSQL Unicode]
  2 +Description = PostgreSQL ODBC driver (Unicode version)
  3 +Driver = /usr/lib/odbc/psqlodbcw.so
  4 +Setup = /usr/lib/odbc/libodbcpsqlS.so
  5 +Debug = 0
  6 +CommLog = 1
  7 +UsageCount = 3