Commit 6946395bf26bd2438ff2f85deb063936651d3b58

Authored by Victor Costa
2 parents babac538 c456fe22

Merge branch 'master' into rails3_AI3033-serpro_integration

Showing 1715 changed files with 110187 additions and 159269 deletions   Show diff stats

Too many changes.

To preserve performance only 100 of 1715 files displayed.

.ackrc
1 1 --ignore-dir=log
2 2 --ignore-dir=tmp
3 3 --ignore-dir=pkg
  4 +--ignore-dir=public/javascripts/cache
  5 +--ignore-dir=public/stylesheets/cache
... ...
... ... @@ -1,251 +0,0 @@
1   -If you are not listed here, but should be, please write to the noosfero mailing
2   -list: http://listas.softwarelivre.org/cgi-bin/mailman/listinfo/noosfero-dev
3   -(this list requires subscription to post, but since you are an author of
4   -noosfero, that's not a problem).
5   -
6   -Developers
7   -==========
8   -
9   -Ábner Silva de Oliveira <abner.oliveira@serpro.gov.br>
10   -Alan Freihof Tygel <alantygel@gmail.com>
11   -alcampelo <alcampelo@alcampelo.(none)>
12   -Alessandro Palmeira <alessandro.palmeira@gmail.com>
13   -Alessandro Palmeira + Caio C. Salgado <alessandro.palmeira@gmail.com>
14   -Alessandro Palmeira + Caio Salgado <alessandro.palmeira@gmail.com>
15   -Alessandro Palmeira + Caio Salgado <caio.csalgado@gmail.com>
16   -Alessandro Palmeira + Caio Salgado + Diego Araújo + João M. M. da Silva <diegoamc90@gmail.com>
17   -Alessandro Palmeira + Carlos Morais <alessandro.palmeira@gmail.com>
18   -Alessandro Palmeira + Daniel Alves <alessandro.palmeira@gmail.com>
19   -Alessandro Palmeira + Daniel Alves + Diego Araújo <diegoamc90@gmail.com>
20   -Alessandro Palmeira + Daniel Alves + Diego Araújo + Guilherme Rojas <danpaulalves@gmail.com>
21   -Alessandro Palmeira + Diego Araujo <alessandro.palmeira@gmail.com>
22   -Alessandro Palmeira + Diego Araújo <alessandro.palmeira@gmail.com>
23   -Alessandro Palmeira + Diego Araujo + Daniela Feitosa <alessandro.palmeira@gmail.com>
24   -Alessandro Palmeira + Diego Araujo <diegoamc90@gmail.com>
25   -Alessandro Palmeira + Diego Araújo <diegoamc90@gmail.com>
26   -Alessandro Palmeira + Diego Araujo + Eduardo Morais <alessandro.palmeira@gmail.com>
27   -Alessandro Palmeira + Diego Araújo + João M. M. da Silva <alessandro.palmeira@gmail.com>
28   -Alessandro Palmeira + Diego Araújo + João M. M. da Silva <diegoamc90@gmail.com>
29   -Alessandro Palmeira + Diego Araujo + João M. M. da Silva + Paulo Meirelles <alessandro.palmeira@gmail.com>
30   -Alessandro Palmeira + Diego Araújo + Pedro Leal <diegoamc90@gmail.com>
31   -Alessandro Palmeira + Diego Araújo + Pedro Leal + João M. M. da Silva <diegoamc90@gmail.com>
32   -Alessandro Palmeira + Diego Araujo + Rafael Manzo <alessandro.palmeira@gmail.com>
33   -Alessandro Palmeira + Eduardo Morais <alessandro.palmeira@gmail.com>
34   -Alessandro Palmeira + Guilherme Rojas <alessandro.palmeira@gmail.com>
35   -Alessandro Palmeira + Jefferson Fernandes <alessandro.palmeira@gmail.com>
36   -Alessandro Palmeira + João M. M. da Silva <alessandro.palmeira@gmail.com>
37   -Alessandro Palmeira + Joao M. M. da Silva + Diego Araujo <alessandro.palmeira@gmail.com>
38   -Alessandro Palmeira + João M. M. da Silva + Renan Teruo <alessandro.palmeira@gmail.com>
39   -Alessandro Palmeira + João M. M. Silva <alessandro.palmeira@gmail.com>
40   -Alessandro Palmeira + Paulo Meirelles <alessandro.palmeira@gmail.com>
41   -Alessandro Palmeira + Paulo Meirelles + João M. M. da Silva <alessandro.palmeira@gmail.com>
42   -Alessandro Palmeira + Rafael Manzo <alessandro.palmeira@gmail.com>
43   -Ana Losnak <analosnak@gmail.com>
44   -Antonio Terceiro + Carlos Morais <terceiro@colivre.coop.br>
45   -Antonio Terceiro + Paulo Meirelles <terceiro@colivre.coop.br>
46   -Antonio Terceiro <terceiro@colivre.coop.br>
47   -Arthur Del Esposte <arthurmde@gmail.com>
48   -Arthur Del Esposte <arthurmde@yahoo.com.br>
49   -Aurelio A. Heckert <aurelio@colivre.coop.br>
50   -Braulio Bhavamitra <brauliobo@gmail.com>
51   -Bráulio Bhavamitra <brauliobo@gmail.com>
52   -Braulio Bhavamitra <braulio@eita.org.br>
53   -Caio <caio.csalgado@gmail.com>
54   -Caio + Diego + Pedro + João <caio.csalgado@gmail.com>
55   -Caio Formiga <caio.formiga@gmail.com>
56   -Caio, Pedro <caio.csalgado@gmail.com>
57   -Caio Salgado + Alessandro Palmeira <caio.csalgado@gmail.com>
58   -Caio Salgado <caio.csalgado@gmail.com>
59   -Caio Salgado + Carlos Morais + Diego Araújo + Pedro Leal <diegoamc90@gmail.com>
60   -Caio Salgado + Diego Araujo <caio.csalgado@gmail.com>
61   -Caio Salgado + Diego Araújo <caio.csalgado@gmail.com>
62   -Caio Salgado + Diego Araújo <diegoamc90@gmail.com>
63   -Caio Salgado + Diego Araújo + Jefferson Fernandes <caio.csalgado@gmail.com>
64   -Caio Salgado + Diego Araújo + João M. M. da Silva <caio.csalgado@gmail.com>
65   -Caio Salgado + Diego Araújo + Pedro Leal <caio.csalgado@gmail.com>
66   -Caio Salgado + Diego Araújo + Pedro Leal <diegoamc90@gmail.com>
67   -Caio Salgado + Diego Araújo + Rafael Manzo <diegoamc90@gmail.com>
68   -Caio Salgado + Jefferson Fernandes <caio.csalgado@gmail.com>
69   -Caio Salgado + Jefferson Fernandes <jeffs.fernandes@gmail.com>
70   -Caio Salgado + Rafael Manzo <caio.csalgado@gmail.com>
71   -Caio Salgado + Renan Teruo <caio.csalgado@gmail.com>
72   -Caio Salgado + Renan Teruo <caio.salgado@gmail.com>
73   -Caio Salgado + Renan Teruo + Jefferson Fernandes <jeffs.fernandes@gmail.com>
74   -Caio Salgado + Renan Teruo <renanteruoc@gmail.com>
75   -Caio SBA <caio@colivre.coop.br>
76   -Caio Tiago Oliveira <caiotiago@colivre.coop.br>
77   -Carlos Andre de Souza <carlos.andre.souza@msn.com>
78   -Carlos Morais <carlos88morais@gmail.com>
79   -Carlos Morais + Diego Araújo <diegoamc90@gmail.com>
80   -Carlos Morais + Eduardo Morais <carlos88morais@gmail.com>
81   -Carlos Morais + Paulo Meirelles <carlos88morais@gmail.com>
82   -Carlos Morais + Pedro Leal <carlos88morais@gmail.com>
83   -Daniela Feitosa <dani@dohko.(none)>
84   -Daniel Alves + Diego Araújo <danpaulalves@gmail.com>
85   -Daniel Alves + Diego Araújo <diegoamc90@gmail.com>
86   -Daniel Alves + Diego Araújo + Guilherme Rojas <danpaulalves@gmail.com>
87   -Daniel Alves + Diego Araújo + Guilherme Rojas <diegoamc90@gmail.com>
88   -Daniel Alves + Diego Araújo + Guilherme Rojas <guilhermehrojas@gmail.com>
89   -Daniel Alves + Guilherme Rojas <danpaulalves@gmail.com>
90   -Daniel Alves + Rafael Manzo <rr.manzo@gmail.com>
91   -Daniela Soares Feitosa <danielafeitosa@colivre.coop.br>
92   -Daniel Bucher <daniel.bucher88@gmail.com>
93   -Daniel Cunha <daniel@colivre.coop.br>
94   -David Carlos <ddavidcarlos1392@gmail.com>
95   -diegoamc <diegoamc90@gmail.com>
96   -Diego Araújo + Alessandro Palmeira <diegoamc90@gmail.com>
97   -Diego Araújo + Alessandro Palmeira + João M. M. da Silva <diegoamc90@gmail.com>
98   -Diego Araújo + Alessandro Palmeira + Rafael Manzo <rr.manzo@gmail.com>
99   -Diego Araujo + Caio Salgado <diegoamc90@gmail.com>
100   -Diego Araújo + Daniel Alves + Rafael Manzo <rr.manzo@gmail.com>
101   -Diego Araújo <diegoamc90@gmail.com>
102   -Diego Araújo + Eduardo Morais + Paulo Meirelles <diegoamc90@gmail.com>
103   -Diego Araújo + Guilherme Rojas <diegoamc90@gmail.com>
104   -Diego Araújo + Jefferson Fernandes <diegoamc90@gmail.com>
105   -Diego Araujo + Jefferson Fernandes <jeffs.fernandes@gmail.com>
106   -Diego Araújo + João Machini <diegoamc90@gmail.com>
107   -Diego Araújo + João Machini <digoamc90@gmail.com>
108   -Diego Araújo + João M. M. da Silva + Alessandro Palmeira <jaodsilv@linux.ime.usp.br>
109   -Diego Araújo + João M. M. da Silva <diegoamc90@gmail.com>
110   -Diego Araújo + João M. M. da Silva + João Machini <diegoamc90@gmail.com>
111   -Diego Araújo + João M. M. da Silva + Pedro Leal <diegoamc90@gmail.com>
112   -Diego Araújo + Paulo Meirelles <diegoamc90@gmail.com>
113   -Diego Araújo + Pedro Leal <diegoamc90@gmail.com>
114   -Diego Araujo + Rafael Manzo <diegoamc90@gmail.com>
115   -Diego Araújo + Rafael Manzo <diegoamc90@gmail.com>
116   -Diego Araújo + Renan Teruo + Alessandro Palmeira <diegoamc90@gmail.com>
117   -Diego Araújo + Renan Teruo <diegoamc90@gmail.com>
118   -Diego Araujo + Rodrigo Souto + Rafael Manzo <rr.manzo@gmail.com>
119   -Diego + Jefferson <diegoamc90@gmail.com>
120   -Diego Martinez <diegoamc90@gmail.com>
121   -Diego Martinez <diego@diego-K55A.(none)>
122   -Diego + Renan <renanteruoc@gmail.com>
123   -Eduardo Tourinho Edington <eduardo.edington@serpro.gov.br>
124   -Evandro Jr <evandrojr@gmail.com>
125   -Evandro Junior <evandrojr@gmail.com>
126   -Fabio Teixeira <fabio1079@gmail.com>
127   -Fernanda Lopes <nanda.listas+psl@gmail.com>
128   -Francisco Marcelo A. Lima Júnior <francisco.lima-junior@serpro.gov.br>
129   -Francisco Marcelo de Araujo Lima Junior <79350259591@serpro-1457614.(none)>
130   -Francisco Marcelo de Araújo Lima Júnior <francisco.lima-junior@serpro.gov.br>
131   -Francisco Marcelo de Araújo Lima Júnior <maljunior@gmail.com>
132   -Gabriela Navarro <navarro1703@gmail.com>
133   -Grazieno Pellegrino <grazieno@gmail.com>
134   -Gust <darksshades@hotmail.com>
135   -Hugo Melo <hugo@riseup.net>
136   -Isaac Canan <isaac@intelletto.com.br>
137   -Italo Valcy <italo@dcc.ufba.br>
138   -Jefferson Fernandes + Diego Araujo + Rafael Manzo <jeffs.fernandes@gmail.com>
139   -Jefferson Fernandes + Joao M. M. da Silva <jeffs.fernandes@gmail.com>
140   -Jefferson Fernandes + Joao M. M. Silva <jeffs.fernandes@gmail.com>
141   -João da Silva + Eduardo Morais + Rafael Manzo <rr.manzo@gmail.com>
142   -João da Silva <jaodsilv@linux.ime.usp.br>
143   -João Marco Maciel da Silva + Rafael Manzo + Renan Teruo <jaodsilv@linux.ime.usp.br>
144   -João M. M. da Silva + Alessandro Palmeira + Diego Araújo + Caio Salgado <jaodsilv@linux.ime.usp.br>
145   -João M. M. da Silva + Alessandro Palmeira + Diego Araújo <jaodsilv@linux.ime.usp.br>
146   -Joao M. M. da Silva + Alessandro Palmeira <jaodsilv@linux.ime.usp.br>
147   -João M. M. da Silva + Alessandro Palmeira <jaodsilv@linux.ime.usp.br>
148   -João M. M. da Silva + Alessandro Palmeira + João Machini <jaodsilv@linux.ime.usp.br>
149   -João M. M. da Silva + Caio Salgado + Alessandro Palmeira <jaodsilv@linux.ime.usp.br>
150   -João M. M. da Silva + Caio Salgado <jaodsilv@linux.ime.usp.br>
151   -João M. M. da Silva + Carlos Morais <jaodsilv@linux.ime.usp.br>
152   -João M. M. da Silva + Diego Araújo <diegoamc90@gmail.com>
153   -João M. M. da Silva + Diego Araújo <jaodsilv@linux.ime.usp.br>
154   -João M. M. da Silva + Diego Araújo + Pedro Leal <jaodsilv@linux.ime.usp.br>
155   -João M. M. da Silva <jaodsilv@linux.ime.usp.br>
156   -Joao M. M. da Silva + Jefferson Fernandes <jaodsilv@linux.ime.usp.br>
157   -João M. M. da Silva + Jefferson Fernandes <jaodsilv@linux.ime.usp.br>
158   -João M. M. da Silva + João M. Miranda <jaodsilv@linux.ime.usp.br>
159   -João M. M. da Silva + Paulo Meirelles <jaodsilv@linux.ime.usp.br>
160   -João M. M. da Silva + Pedro Leal <jaodsilv@linux.ime.usp.br>
161   -João M. M. da Silva + Rafael Manzo + Diego Araújo <jaodsilv@linux.ime.usp.br>
162   -João M. M. da Silva + Rafael Manzo <jaodsilv@linux.ime.usp.br>
163   -João M. M. da Silva + Renan Teruo <jaodsilv@linux.ime.usp.br>
164   -João M. M. Silva + Caio Salgado <jaodsilv@linux.ime.usp.br>
165   -João M. M. Silva + Diego Araújo <jaodsilv@linux.ime.usp.br>
166   -Joao M. M. Silva + Jefferson Fernandes <jaodsilv@linux.ime.usp.br>
167   -João M. M. Silva + Paulo Meirelles <jaodsilv@linux.ime.usp.br>
168   -João M. M. Silva + Rafael Manzo <jaodsilv@linux.ime.usp.br>
169   -João M. M. Silva + Renan Teruo <jaodsilv@linux.ime.usp.br>
170   -Joenio Costa <joenio@colivre.coop.br>
171   -Josef Spillner <josef.spillner@tu-dresden.de>
172   -Junior Silva <junior@bajor.localhost.localdomain>
173   -Junior Silva <junior@sedeantigo.colivre.coop.br>
174   -Junior Silva <juniorsilva1001@gmail.com>
175   -Junior Silva <juniorsilva7@juniorsilva-Aspire-5750Z.(none)>
176   -Junior Silva <juniorsilva@colivre.coop.br>
177   -juniorsilva <juniorsilva@QonoS.localhost.localdomain>
178   -Keilla Menezes <keilla@colivre.coop.br>
179   -Larissa Reis <larissa@colivre.coop.br>
180   -Larissa Reis <reiss.larissa@gmail.com>
181   -Leandro Nunes dos Santos <81665687568@serpro-1541727.Home>
182   -Leandro Nunes dos Santos <81665687568@serpro-1541727.(none)>
183   -Leandro Nunes dos Santos <leandronunes@gmail.com>
184   -Leandro Nunes dos Santos <leandro.santos@serpro.gov.br>
185   -LinguÁgil 2010 <linguagil.bahia@gmail.com>
186   -Lucas Melo <lucas@colivre.coop.br>
187   -Lucas Melo <lucaspradomelo@gmail.com>
188   -Luciano <lucianopcbr@gmail.com>
189   -Luis David Aguilar Carlos <ludwig9003@gmail.com>
190   -Luiz Fernando de Freitas Matos <luiz@luizff.matos@gmail.com>
191   -Marcos Ramos <ms.ramos@outlook.com>
192   -Martín Olivera <molivera@solar.org.ar>
193   -Moises Machado <moises@colivre.coop.br>
194   -Naíla Alves <naila@colivre.coop.br>
195   -Nanda Lopes <nanda.listas+psl@gmail.com>
196   -Paulo Meirelles + Alessandro Palmeira + João M. M. da Silva <paulo@softwarelivre.org>
197   -Paulo Meirelles + Alessandro Palmeira <paulo@softwarelivre.org>
198   -Paulo Meirelles + Carlos Morais <paulo@softwarelivre.org>
199   -Paulo Meirelles + Diego Araújo <paulo@softwarelivre.org>
200   -Paulo Meirelles + João M. M. da Silva <paulo@softwarelivre.org>
201   -Paulo Meirelles <paulo@softwarelivre.org>
202   -Paulo Meirelles + Rafael Manzo <paulo@softwarelivre.org>
203   -Rafael Gomes <rafaelgomes@techfree.com.br>
204   -Rafael Manzo + Alessandro Palmeira <rr.manzo@gmail.com>
205   -Rafael Manzo + Daniel Alves <danpaulalves@gmail.com>
206   -Rafael Manzo + Diego Araújo <rr.manzo@gmail.com>
207   -Rafael Manzo + João M. M. Silva <rr.manzo@gmail.com>
208   -Rafael Manzo + Paulo Meirelles <rr.manzo@gmail.com>
209   -Rafael Martins <rmmartins@gmail.com>
210   -Rafael Reggiani Manzo + Caio Salgado + Jefferson Fernandes <rr.manzo@gmail.com>
211   -Rafael Reggiani Manzo + Diego Araujo <diegoamc90@gmail.com>
212   -Rafael Reggiani Manzo + Diego Araujo <rr.manzo@gmail.com>
213   -Rafael Reggiani Manzo + Diego Araújo <rr.manzo@gmail.com>
214   -Rafael Reggiani Manzo + João M. M. da Silva <rr.manzo@gmail.com>
215   -Rafael Reggiani Manzo <rr.manzo@gmail.com>
216   -Raphaël Rousseau <raph@r4f.org>
217   -Raquel Lira <raquel.lira@gmail.com>
218   -Renan Teruo + Caio Salgado <renanteruoc@gmail.com>
219   -Renan Teruoc + Diego Araujo <renanteruoc@gmail.com>
220   -Renan Teruo + Diego Araujo <renanteruoc@gmail.com>
221   -Renan Teruo + Diego Araújo <renanteruoc@gmail.com>
222   -Renan Teruo + Paulo Meirelles <renanteruoc@gmail.com>
223   -Renan Teruo + Rafael Manzo <renanteruoc@gmail.com>
224   -Rodrigo Souto + Ana Losnak + Daniel Bucher + Caio Almeida + Leandro Nunes + Daniela Feitosa + Mariel Zasso <noosfero-br@listas.softwarelivre.org>
225   -Rodrigo Souto <diguliu@gmail.com>
226   -Rodrigo Souto <rodrigo@colivre.coop.br>
227   -Ronny Kursawe <kursawe.ronny@googlemail.com>
228   -root <root@debian.sdr.serpro>
229   -Samuel R. C. Vale <srcvale@holoscopio.com>
230   -Valessio Brito <contato@valessiobrito.com.br>
231   -Valessio Brito <contato@valessiobrito.info>
232   -Valessio Brito <valessio@gmail.com>
233   -vfcosta <vfcosta@gmail.com>
234   -Victor Carvalho <victorhugodf.ac@gmail.com>
235   -Victor Costa <vfcosta@gmail.com>
236   -Victor Hugo Alves de Carvalho <victorhugodf.ac@gmail.com>
237   -Vinicius Cubas Brand <viniciuscb@gmail.com>
238   -Visita <visita@debian.(none)>
239   -Yann Lugrin <yann.lugrin@liquid-concept.ch>
240   -
241   -Ideas, specifications and incentive
242   -===================================
243   -Daniel Tygel <dtygel@fbes.org.br>
244   -Guilherme Rocha <guilherme@gf7.com.br>
245   -Raphael Rousseau <raph@r4f.org>
246   -Théo Bondolfi <move@cooperation.net>
247   -Vicente Aguiar <vicenteaguiar@colivre.coop.br>
248   -
249   -Arts
250   -===================================
251   -Nara Oliveira <narananet@gmail.com>
AUTHORS.md
1   -If you are not listed here, but should be, please write to the noosfero mailing list: http://listas.softwarelivre.org/cgi-bin/mailman/listinfo/noosfero-dev (this list requires subscription to post, but since you are an author of noosfero, that's not a problem).
  1 +If you are not listed here, but should be, please write to the noosfero mailing
  2 +list: http://listas.softwarelivre.org/cgi-bin/mailman/listinfo/noosfero-dev
  3 +(this list requires subscription to post, but since you are an author of
  4 +noosfero, that's not a problem).
2 5  
3 6 Developers
4 7 ==========
5 8  
  9 +Ábner Silva de Oliveira <abner.oliveira@serpro.gov.br>
6 10 Alan Freihof Tygel <alantygel@gmail.com>
  11 +alcampelo <alcampelo@alcampelo.(none)>
7 12 Alessandro Palmeira <alessandro.palmeira@gmail.com>
8 13 Alessandro Palmeira + Caio C. Salgado <alessandro.palmeira@gmail.com>
9 14 Alessandro Palmeira + Caio Salgado <alessandro.palmeira@gmail.com>
... ... @@ -35,9 +40,14 @@ Alessandro Palmeira + João M. M. Silva &lt;alessandro.palmeira@gmail.com&gt;
35 40 Alessandro Palmeira + Paulo Meirelles <alessandro.palmeira@gmail.com>
36 41 Alessandro Palmeira + Paulo Meirelles + João M. M. da Silva <alessandro.palmeira@gmail.com>
37 42 Alessandro Palmeira + Rafael Manzo <alessandro.palmeira@gmail.com>
  43 +analosnak <analosnak@gmail.com>
  44 +Ana Losnak <analosnak@gmail.com>
  45 +Andre Bernardes <andrebsguedes@gmail.com>
38 46 Antonio Terceiro + Carlos Morais <terceiro@colivre.coop.br>
39 47 Antonio Terceiro + Paulo Meirelles <terceiro@colivre.coop.br>
40 48 Antonio Terceiro <terceiro@colivre.coop.br>
  49 +Arthur Del Esposte <arthurmde@gmail.com>
  50 +Arthur Del Esposte <arthurmde@yahoo.com.br>
41 51 Aurelio A. Heckert <aurelio@colivre.coop.br>
42 52 Braulio Bhavamitra <brauliobo@gmail.com>
43 53 Bráulio Bhavamitra <brauliobo@gmail.com>
... ... @@ -65,6 +75,8 @@ Caio Salgado + Renan Teruo &lt;caio.salgado@gmail.com&gt;
65 75 Caio Salgado + Renan Teruo + Jefferson Fernandes <jeffs.fernandes@gmail.com>
66 76 Caio Salgado + Renan Teruo <renanteruoc@gmail.com>
67 77 Caio SBA <caio@colivre.coop.br>
  78 +Caio Tiago Oliveira <caiotiago@colivre.coop.br>
  79 +Carlos Andre de Souza <carlos.andre.souza@msn.com>
68 80 Carlos Morais <carlos88morais@gmail.com>
69 81 Carlos Morais + Diego Araújo <diegoamc90@gmail.com>
70 82 Carlos Morais + Eduardo Morais <carlos88morais@gmail.com>
... ... @@ -78,7 +90,9 @@ Daniel Alves + Diego Araújo + Guilherme Rojas &lt;guilhermehrojas@gmail.com&gt;
78 90 Daniel Alves + Guilherme Rojas <danpaulalves@gmail.com>
79 91 Daniel Alves + Rafael Manzo <rr.manzo@gmail.com>
80 92 Daniela Soares Feitosa <danielafeitosa@colivre.coop.br>
  93 +Daniel Bucher <daniel.bucher88@gmail.com>
81 94 Daniel Cunha <daniel@colivre.coop.br>
  95 +David Carlos <ddavidcarlos1392@gmail.com>
82 96 diegoamc <diegoamc90@gmail.com>
83 97 Diego Araújo + Alessandro Palmeira <diegoamc90@gmail.com>
84 98 Diego Araújo + Alessandro Palmeira + João M. M. da Silva <diegoamc90@gmail.com>
... ... @@ -105,17 +119,27 @@ Diego Araújo + Renan Teruo &lt;diegoamc90@gmail.com&gt;
105 119 Diego Araujo + Rodrigo Souto + Rafael Manzo <rr.manzo@gmail.com>
106 120 Diego + Jefferson <diegoamc90@gmail.com>
107 121 Diego Martinez <diegoamc90@gmail.com>
108   -Diego Martinez <diego@diego-K55A.(none)>
109 122 Diego + Renan <renanteruoc@gmail.com>
  123 +Eduardo Tourinho Edington <eduardo.edington@serpro.gov.br>
  124 +Evandro Jr <evandrojr@gmail.com>
  125 +Evandro Junior <evandrojr@gmail.com>
  126 +Fabio Teixeira <fabio1079@gmail.com>
110 127 Fernanda Lopes <nanda.listas+psl@gmail.com>
111 128 Francisco Marcelo A. Lima Júnior <francisco.lima-junior@serpro.gov.br>
112 129 Francisco Marcelo de Araujo Lima Junior <79350259591@serpro-1457614.(none)>
  130 +Francisco Marcelo de Araújo Lima Júnior <francisco.lima-junior@serpro.gov.br>
  131 +Francisco Marcelo de Araújo Lima Júnior <maljunior@gmail.com>
  132 +Gabriela Navarro <navarro1703@gmail.com>
113 133 Grazieno Pellegrino <grazieno@gmail.com>
  134 +Gust <darksshades@hotmail.com>
  135 +Hebert Douglas <hebertdougl@gmail.com>
  136 +Hugo Melo <hugo@riseup.net>
114 137 Isaac Canan <isaac@intelletto.com.br>
115 138 Italo Valcy <italo@dcc.ufba.br>
116 139 Jefferson Fernandes + Diego Araujo + Rafael Manzo <jeffs.fernandes@gmail.com>
117 140 Jefferson Fernandes + Joao M. M. da Silva <jeffs.fernandes@gmail.com>
118 141 Jefferson Fernandes + Joao M. M. Silva <jeffs.fernandes@gmail.com>
  142 +João da Silva + Eduardo Morais + Rafael Manzo <rr.manzo@gmail.com>
119 143 João da Silva <jaodsilv@linux.ime.usp.br>
120 144 João Marco Maciel da Silva + Rafael Manzo + Renan Teruo <jaodsilv@linux.ime.usp.br>
121 145 João M. M. da Silva + Alessandro Palmeira + Diego Araújo + Caio Salgado <jaodsilv@linux.ime.usp.br>
... ... @@ -146,21 +170,35 @@ João M. M. Silva + Rafael Manzo &lt;jaodsilv@linux.ime.usp.br&gt;
146 170 João M. M. Silva + Renan Teruo <jaodsilv@linux.ime.usp.br>
147 171 Joenio Costa <joenio@colivre.coop.br>
148 172 Josef Spillner <josef.spillner@tu-dresden.de>
  173 +Jose Pedro <1jpsneto@gmail.com>
  174 +Junior Silva <junior@bajor.localhost.localdomain>
  175 +Junior Silva <junior@sedeantigo.colivre.coop.br>
149 176 Junior Silva <juniorsilva1001@gmail.com>
150 177 Junior Silva <juniorsilva7@juniorsilva-Aspire-5750Z.(none)>
  178 +Junior Silva <juniorsilva@colivre.coop.br>
  179 +juniorsilva <juniorsilva@QonoS.localhost.localdomain>
151 180 Keilla Menezes <keilla@colivre.coop.br>
152 181 Larissa Reis <larissa@colivre.coop.br>
153 182 Larissa Reis <reiss.larissa@gmail.com>
  183 +Leandro Alves <leandrosustenido@gmail.com>
  184 +Leandro Nunes dos Santos <81665687568@serpro-1541727.Home>
  185 +Leandro Nunes dos Santos <81665687568@serpro-1541727.(none)>
154 186 Leandro Nunes dos Santos <leandronunes@gmail.com>
155 187 Leandro Nunes dos Santos <leandro.santos@serpro.gov.br>
156 188 LinguÁgil 2010 <linguagil.bahia@gmail.com>
157 189 Lucas Melo <lucas@colivre.coop.br>
158 190 Lucas Melo <lucaspradomelo@gmail.com>
  191 +Luciano <lucianopcbr@gmail.com>
  192 +Luciano Prestes Cavalcanti <lucianopcbr@gmail.com>
159 193 Luis David Aguilar Carlos <ludwig9003@gmail.com>
  194 +Luiz Fernando de Freitas Matos <luiz@luizff.matos@gmail.com>
  195 +Marcos Ramos <ms.ramos@outlook.com>
160 196 Martín Olivera <molivera@solar.org.ar>
  197 +Michal Čihař <michal@cihar.com>
161 198 Moises Machado <moises@colivre.coop.br>
162 199 Naíla Alves <naila@colivre.coop.br>
163 200 Nanda Lopes <nanda.listas+psl@gmail.com>
  201 +Parley Martins <parleypachecomartins@gmail.com>
164 202 Paulo Meirelles + Alessandro Palmeira + João M. M. da Silva <paulo@softwarelivre.org>
165 203 Paulo Meirelles + Alessandro Palmeira <paulo@softwarelivre.org>
166 204 Paulo Meirelles + Carlos Morais <paulo@softwarelivre.org>
... ... @@ -183,20 +221,28 @@ Rafael Reggiani Manzo + João M. M. da Silva &lt;rr.manzo@gmail.com&gt;
183 221 Rafael Reggiani Manzo <rr.manzo@gmail.com>
184 222 Raphaël Rousseau <raph@r4f.org>
185 223 Raquel Lira <raquel.lira@gmail.com>
  224 +Raquel <rcordioli@gmail.com>
186 225 Renan Teruo + Caio Salgado <renanteruoc@gmail.com>
187 226 Renan Teruoc + Diego Araujo <renanteruoc@gmail.com>
188 227 Renan Teruo + Diego Araujo <renanteruoc@gmail.com>
189 228 Renan Teruo + Diego Araújo <renanteruoc@gmail.com>
190 229 Renan Teruo + Paulo Meirelles <renanteruoc@gmail.com>
191 230 Renan Teruo + Rafael Manzo <renanteruoc@gmail.com>
192   -Rodrigo Souto <diguliu@gmail.com>
  231 +Rodrigo Souto + Ana Losnak + Daniel Bucher + Caio Almeida + Leandro Nunes + Daniela Feitosa + Mariel Zasso <noosfero-br@listas.softwarelivre.org>
193 232 Rodrigo Souto <rodrigo@colivre.coop.br>
194 233 Ronny Kursawe <kursawe.ronny@googlemail.com>
195 234 root <root@debian.sdr.serpro>
196 235 Samuel R. C. Vale <srcvale@holoscopio.com>
  236 +Tallys Martins <tallysmartins@gmail.com>
  237 +tallys <tallys@tallys.(none)>
  238 +Thiago Zoroastro <thiago.zoroastro@bol.com.br>
  239 +Valessio Brito <contato@valessiobrito.com.br>
  240 +Valessio Brito <contato@valessiobrito.info>
197 241 Valessio Brito <valessio@gmail.com>
198 242 vfcosta <vfcosta@gmail.com>
  243 +Victor Carvalho <victorhugodf.ac@gmail.com>
199 244 Victor Costa <vfcosta@gmail.com>
  245 +Victor Hugo Alves de Carvalho <victorhugodf.ac@gmail.com>
200 246 Vinicius Cubas Brand <viniciuscb@gmail.com>
201 247 Visita <visita@debian.(none)>
202 248 Yann Lugrin <yann.lugrin@liquid-concept.ch>
... ...
DEVELOPMENT.md 0 → 100644
... ... @@ -0,0 +1,124 @@
  1 +# Noosfero Development Policy
  2 +
  3 +## Developer Roles
  4 +
  5 +* *Developers* are everyone that is contributing code to Noosfero.
  6 +* *Committers* are the people with direct commit access to the Noosfero source
  7 + code. They are responsible for reviewing contributions from other developers
  8 + and integrating them in the Noosfero code base. They are the members of the
  9 + [Noosfero group on Gitlab](https://gitlab.com/groups/noosfero/members).
  10 +* *Release managers* are the people that are managing the release of a new
  11 + Noosfero version and/or the maintainance work of an existing Noosfero stable
  12 + branch. See MAINTAINANCE.md for details on the maintaince policy.
  13 +
  14 +## Development process
  15 +
  16 +* Every new feature or non-trivial bugfix should be reviewed by at least one
  17 + committer. This must be the case even if the original author is a committer.
  18 +
  19 + * In the case the original author is a committer, he/she should feel free to
  20 + commit directly if after 1 week nobody has provided any kind of feedback.
  21 +
  22 + * Developers who are not committers should feel free to ping committers if
  23 + they do not get feedback on their contributions after 1 week.
  24 +
  25 + * On GitLab, one can just add a comment to the merge request; one can also
  26 + @-mention specific committers or other developers who have expertise on
  27 + the area of the contribution.
  28 +
  29 + * Committers should follow the activity of the project, and try to help
  30 + reviewing contributions from others as much as possible.
  31 +
  32 + * On GitLab one can get emails for all activity on a project by setting the
  33 + [notification settings](https://gitlab.com/profile/notifications) to
  34 + "watch".
  35 +
  36 + * Anyone can help by reviewing contributions. Committers are the only ones
  37 + who can give the final approval to a contribution, but everyone is welcome
  38 + to help with code review, testing, etc.
  39 +
  40 + * See note above about setting up notification on GitLab.
  41 +
  42 +* Committers should feel free to push trivial (or urgent) changes directly.
  43 + There are no strict rule on what makes a change trivial or urgent; committers
  44 + are expected to exercise good judgement on a case by case basis.
  45 +
  46 + * Usually changes to the database are not trivial.
  47 +
  48 +* In the case of unsolvable conflict between commiters regarding any change to
  49 + the code, the current release manager(s) will have the final say in the
  50 + matter.
  51 +
  52 +* Release managers are responsible for stablishing a release schedule, and
  53 + about deciding when and what to release.
  54 +
  55 + * Release managers should announce release schedules to the project mailing
  56 + lists in advance.
  57 +
  58 + * The release schedule may include a period of feature freeze, during which
  59 + no new features or any other changes that are not pre-approved by the
  60 + release manager must be committed to the repository.
  61 +
  62 + * Committers must respect the release schedule and feature freezes.
  63 +
  64 +## Maintainance process
  65 +
  66 +### Not all feature releases will be maintained as a stable release
  67 +
  68 +We will be choosing specific release series to be maintained as stable
  69 +releases.
  70 +
  71 +This means that a given release is not guaranteed to be maintained as a stable
  72 +release, but does *not* mean it won't be. Any committer (or anyone, really) can
  73 +decide to maintain a given release as stable and seek help from others to do
  74 +so.
  75 +
  76 +### No merges from stable branches to master
  77 +
  78 +*All* changes must be submitted against the master branch first, and when
  79 +applicable, backported to the desired stable releases. Exceptions to this rules
  80 +are bug fixes that only apply to a given stable branch and not to master.
  81 +
  82 +In the past we had non-trivial changes accepted into stable releases while
  83 +master was way ahead (e.g. during the rails3 migration period), that made the
  84 +merge back into master very painful. By eliminating the need to do these
  85 +merges, we save time for the people responsible for the release, and eliminate
  86 +the possibility of human errors or oversights causing changes to be accepted
  87 +into stable that will be a problem to merge back into master.
  88 +
  89 +By getting all fixes in master first, we improve the chances that a future
  90 +release will not present regressions against bugs that should already be fixed,
  91 +but the fixes got lost in a big, complicated merge (and those won't exist
  92 +anymore, at least not from stable branches to master).
  93 +
  94 +After a fix gets into master, backporting changes into a stable release branch
  95 +is the responsibility of whoever is maintaing that branch, and those interested
  96 +in it. The stable branch release manager(s) are entitled the final say on any
  97 +matters related to that branch.
  98 +
  99 +## Apendix A: how to become a committer
  100 +
  101 +Every developer that wants to be a committer should create [an issue on
  102 +Gitlab](https://gitlab.com/noosfero/noosfero/issues) requesting to be added as
  103 +a committer. This request must include information about the requestor's
  104 +previous contributions to the project.
  105 +
  106 +If 2 or more commiters consider second the request, the requestor is accepted
  107 +as new commiter and added to the Noosfero group.
  108 +
  109 +The existing committers are free to choose whatever criteria they want to
  110 +second the request, but they must be sure that the new committer is a
  111 +responsible developer and knows what she/he is doing. They must be aware that
  112 +seconding these requests means seconding the actions of the new committer: if
  113 +the new committer screw up, her/his seconds screwed up.
  114 +
  115 +## Apendix B: how to become a release manager
  116 +
  117 +A new release manager for the development version of Noosfero (i.e. the one
  118 +that includes new features, a.k.a. the master branch) is apointed by the
  119 +current release manager, and must be a committer first.
  120 +
  121 +Release managers for stable branches are self-appointed, i.e. whoever takes the
  122 +work takes the role. In case of a conflict (e.g. 2+ different people want to do
  123 +the work but can't agree on working together), the development release manager
  124 +decides.
... ...
Gemfile
1 1 source "https://rubygems.org"
2   -gem 'rails'
3   -gem 'fast_gettext'
4   -gem 'acts-as-taggable-on'
5   -gem 'prototype-rails'
6   -gem 'prototype_legacy_helper', '0.0.0', :path => 'vendor/prototype_legacy_helper'
7   -gem 'rails_autolink'
8   -gem 'pg'
9   -gem 'rmagick'
10   -gem 'RedCloth'
11   -gem 'will_paginate'
12   -gem 'ruby-feedparser'
13   -gem 'daemons'
14   -gem 'thin'
15   -gem 'hpricot'
16   -gem 'nokogiri'
  2 +gem 'rails', '~> 3.2.21'
  3 +gem 'minitest', '~> 3.2.0'
  4 +gem 'fast_gettext', '~> 0.6.8'
  5 +gem 'acts-as-taggable-on', '~> 3.0.2'
  6 +gem 'prototype-rails', '~> 3.2.1'
  7 +gem 'prototype_legacy_helper', '0.0.0', :path => 'vendor/prototype_legacy_helper'
  8 +gem 'rails_autolink', '~> 1.1.5'
  9 +gem 'pg', '~> 0.13.2'
  10 +gem 'rmagick', '~> 2.13.1'
  11 +gem 'RedCloth', '~> 4.2.9'
  12 +gem 'will_paginate', '~> 3.0.3'
  13 +gem 'ruby-feedparser', '~> 0.7'
  14 +gem 'daemons', '~> 1.1.5'
  15 +gem 'thin', '~> 1.3.1'
  16 +gem 'hpricot', '~> 0.8.6'
  17 +gem 'nokogiri', '~> 1.5.5'
17 18 gem 'rake', :require => false
18   -gem 'rest-client'
19   -gem 'exception_notification'
  19 +gem 'rest-client', '~> 1.6.7'
  20 +gem 'exception_notification', '~> 4.0.1'
  21 +gem 'gettext', '~> 2.2.1', :require => false, :group => :development
  22 +gem 'locale', '~> 2.0.5'
  23 +
  24 +gem 'whenever', :require => false
20 25  
21 26 # FIXME list here all actual dependencies (i.e. the ones in debian/control),
22 27 # with their GEM names (not the Debian package names)
23 28  
24 29 group :production do
25   - gem 'dalli'
  30 + gem 'dalli', '~> 2.7.0'
26 31 end
27 32  
28 33 group :test do
29   - gem 'rspec'
30   - gem 'rspec-rails'
31   - gem 'mocha', :require => false
  34 + gem 'rspec', '~> 2.10.0'
  35 + gem 'rspec-rails', '~> 2.10.1'
  36 + gem 'mocha', '~> 1.1.0', :require => false
32 37 end
33 38  
34 39 group :cucumber do
35   - gem 'cucumber-rails', :require => false
36   - gem 'capybara'
37   - gem 'cucumber'
38   - gem 'database_cleaner'
39   - gem 'selenium-webdriver'
  40 + gem 'cucumber-rails', '~> 1.0.6', :require => false
  41 + gem 'capybara', '~> 2.1.0'
  42 + gem 'cucumber', '~> 1.0.6'
  43 + gem 'database_cleaner', '~> 1.2.0'
  44 + gem 'selenium-webdriver', '~> 2.39.0'
40 45 end
41 46  
42   -# include plugin gemfiles
43   -Dir.glob(File.join('config', 'plugins', '*')).each do |plugin|
44   - plugin_gemfile = File.join(plugin, 'Gemfile')
45   - eval File.read(plugin_gemfile) if File.exists?(plugin_gemfile)
  47 +# include gemfiles from enabled plugins
  48 +# plugins in baseplugins/ are not included on purpose. They should not have any
  49 +# dependencies.
  50 +Dir.glob('config/plugins/*/Gemfile').each do |gemfile|
  51 + eval File.read(gemfile)
46 52 end
... ...
Gemfile.lock
... ... @@ -1,191 +0,0 @@
1   -PATH
2   - remote: vendor/prototype_legacy_helper
3   - specs:
4   - prototype_legacy_helper (0.0.0)
5   -
6   -GEM
7   - remote: https://rubygems.org/
8   - specs:
9   - RedCloth (4.2.9)
10   - actionmailer (3.2.6)
11   - actionpack (= 3.2.6)
12   - mail (~> 2.4.4)
13   - actionpack (3.2.6)
14   - activemodel (= 3.2.6)
15   - activesupport (= 3.2.6)
16   - builder (~> 3.0.0)
17   - erubis (~> 2.7.0)
18   - journey (~> 1.0.1)
19   - rack (~> 1.4.0)
20   - rack-cache (~> 1.2)
21   - rack-test (~> 0.6.1)
22   - sprockets (~> 2.1.3)
23   - activemodel (3.2.6)
24   - activesupport (= 3.2.6)
25   - builder (~> 3.0.0)
26   - activerecord (3.2.6)
27   - activemodel (= 3.2.6)
28   - activesupport (= 3.2.6)
29   - arel (~> 3.0.2)
30   - tzinfo (~> 0.3.29)
31   - activeresource (3.2.6)
32   - activemodel (= 3.2.6)
33   - activesupport (= 3.2.6)
34   - activesupport (3.2.6)
35   - i18n (~> 0.6)
36   - multi_json (~> 1.0)
37   - acts-as-taggable-on (3.0.2)
38   - rails (>= 3, < 5)
39   - arel (3.0.2)
40   - builder (3.0.0)
41   - capybara (2.1.0)
42   - mime-types (>= 1.16)
43   - nokogiri (>= 1.3.3)
44   - rack (>= 1.0.0)
45   - rack-test (>= 0.5.4)
46   - xpath (~> 2.0)
47   - childprocess (0.3.3)
48   - ffi (~> 1.0.6)
49   - cucumber (1.0.6)
50   - builder (>= 2.1.2)
51   - diff-lcs (>= 1.1.2)
52   - gherkin (~> 2.4.18)
53   - json (>= 1.4.6)
54   - term-ansicolor (>= 1.0.6)
55   - cucumber-rails (1.0.6)
56   - capybara (>= 1.1.1)
57   - cucumber (>= 1.0.6)
58   - nokogiri (>= 1.5.0)
59   - daemons (1.1.5)
60   - dalli (2.7.0)
61   - database_cleaner (1.2.0)
62   - diff-lcs (1.1.3)
63   - erubis (2.7.0)
64   - eventmachine (0.12.10)
65   - exception_notification (4.0.1)
66   - actionmailer (>= 3.0.4)
67   - activesupport (>= 3.0.4)
68   - fast_gettext (0.6.8)
69   - ffi (1.0.11)
70   - gherkin (2.4.21)
71   - json (>= 1.4.6)
72   - hike (1.2.1)
73   - hpricot (0.8.6)
74   - i18n (0.6.0)
75   - journey (1.0.3)
76   - json (1.7.3)
77   - mail (2.4.4)
78   - i18n (>= 0.4.0)
79   - mime-types (~> 1.16)
80   - treetop (~> 1.4.8)
81   - metaclass (0.0.1)
82   - mime-types (1.19)
83   - mocha (0.11.3)
84   - metaclass (~> 0.0.1)
85   - multi_json (1.3.6)
86   - nokogiri (1.5.5)
87   - pg (0.13.2)
88   - polyglot (0.3.3)
89   - prototype-rails (3.2.1)
90   - rails (~> 3.2)
91   - rack (1.4.1)
92   - rack-cache (1.2)
93   - rack (>= 0.4)
94   - rack-ssl (1.3.2)
95   - rack
96   - rack-test (0.6.1)
97   - rack (>= 1.0)
98   - rails (3.2.6)
99   - actionmailer (= 3.2.6)
100   - actionpack (= 3.2.6)
101   - activerecord (= 3.2.6)
102   - activeresource (= 3.2.6)
103   - activesupport (= 3.2.6)
104   - bundler (~> 1.0)
105   - railties (= 3.2.6)
106   - rails_autolink (1.1.5)
107   - rails (> 3.1)
108   - railties (3.2.6)
109   - actionpack (= 3.2.6)
110   - activesupport (= 3.2.6)
111   - rack-ssl (~> 1.3.2)
112   - rake (>= 0.8.7)
113   - rdoc (~> 3.4)
114   - thor (>= 0.14.6, < 2.0)
115   - rake (0.9.2.2)
116   - rdoc (3.9.4)
117   - rest-client (1.6.7)
118   - mime-types (>= 1.16)
119   - rmagick (2.13.1)
120   - rspec (2.10.0)
121   - rspec-core (~> 2.10.0)
122   - rspec-expectations (~> 2.10.0)
123   - rspec-mocks (~> 2.10.0)
124   - rspec-core (2.10.1)
125   - rspec-expectations (2.10.0)
126   - diff-lcs (~> 1.1.3)
127   - rspec-mocks (2.10.1)
128   - rspec-rails (2.10.1)
129   - actionpack (>= 3.0)
130   - activesupport (>= 3.0)
131   - railties (>= 3.0)
132   - rspec (~> 2.10.0)
133   - ruby-feedparser (0.7)
134   - rubyzip (1.1.2)
135   - selenium-webdriver (2.39.0)
136   - childprocess (>= 0.2.5)
137   - multi_json (~> 1.0)
138   - rubyzip (~> 1.0)
139   - websocket (~> 1.0.4)
140   - sprockets (2.1.3)
141   - hike (~> 1.2)
142   - multi_json (~> 1.0)
143   - rack (~> 1.0)
144   - tilt (~> 1.1, != 1.3.0)
145   - term-ansicolor (1.0.7)
146   - thin (1.3.1)
147   - daemons (>= 1.0.9)
148   - eventmachine (>= 0.12.6)
149   - rack (>= 1.0.0)
150   - thor (0.15.3)
151   - tilt (1.3.3)
152   - treetop (1.4.10)
153   - polyglot
154   - polyglot (>= 0.3.1)
155   - tzinfo (0.3.33)
156   - websocket (1.0.7)
157   - will_paginate (3.0.3)
158   - xpath (2.0.0)
159   - nokogiri (~> 1.3)
160   -
161   -PLATFORMS
162   - ruby
163   -
164   -DEPENDENCIES
165   - RedCloth
166   - acts-as-taggable-on
167   - capybara
168   - cucumber
169   - cucumber-rails
170   - daemons
171   - dalli
172   - database_cleaner
173   - exception_notification
174   - fast_gettext
175   - hpricot
176   - mocha
177   - nokogiri
178   - pg
179   - prototype-rails
180   - prototype_legacy_helper (= 0.0.0)!
181   - rails
182   - rails_autolink
183   - rake
184   - rest-client
185   - rmagick
186   - rspec
187   - rspec-rails
188   - ruby-feedparser
189   - selenium-webdriver
190   - thin
191   - will_paginate
INSTALL.https.md
1   -Setup Noosfero to use HTTPS
2   -===========================
  1 +# Setup Noosfero to use HTTPS
3 2  
4 3 This document assumes that you have a fully and clean Noosfero
5 4 installation as explained at the `INSTALL.md` file.
6 5  
7   -SSL certificate
8   -+++++++++++++++
  6 +## Creating a self-signed SSL certificate
9 7  
10 8 You should get a valid SSL certificate, but if you want to test
11 9 your setup before, you could generate a self-signed certificate
... ... @@ -17,99 +15,106 @@ as below:
17 15 # openssl req -new -x509 -nodes -sha1 -days $[10*365] -key noosfero.key > noosfero.cert
18 16 # cat noosfero.key noosfero.cert > noosfero.pem
19 17  
  18 +## Web server configuration
  19 +
20 20 There are two ways of using SSL with Noosfero: 1) If you are not using
21 21 Varnish; and 2) If you are using Varnish.
22 22  
23   -1) If you are are not using Varnish
24   -+++++++++++++++++++++++++++++++++++
  23 +### 1) If you are are not using Varnish
25 24  
26 25 Simply do a redirect in apache to force all connections with SSL:
27 26  
28   - <VirtualHost *:8080>
29   - ServerName test.stoa.usp.br
30   -
31   - Redirect / https://example.com/
32   - </VirtualHost>
  27 +```
  28 +<VirtualHost *:8080>
  29 + ServerName test.stoa.usp.br
  30 + Redirect / https://example.com/
  31 +</VirtualHost>
  32 +```
33 33  
34 34 And set a vhost to receive then:
35 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>
  36 +```
  37 +<VirtualHost *:443>
  38 + ServerName example.com
  39 + SSLEngine On
  40 + SSLCertificateFile /etc/ssl/certs/cert.pem
  41 + SSLCertificateKeyFile /etc/ssl/private/cert.key
  42 + Include /etc/noosfero/apache/virtualhost.conf
  43 +</VirtualHost>
  44 +```
45 45  
46 46 Be aware that if you had configured varnish, the requests won't reach
47 47 it with this configuration.
48 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:
  49 +### 2) If you are using Varnish
55 50  
56   - $ sudo apt-get install pound
  51 +Varnish isn't able to communicate with the SSL protocol, so we will need some
  52 +one else who do this and [Pound](http://www.apsis.ch/pound) can do the job. In
  53 +order to install it in Debian based systems:
57 54  
58   -Set Varnish to listen in other port than 80:
  55 +```
  56 +$ sudo apt-get install pound
  57 +```
59 58  
60   -/etc/defaults/varnish
61   ----------------------
  59 +Set Varnish to listen in other port than 80 in `/etc/defaults/varnish`:
62 60  
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"
  61 +```
  62 +DAEMON_OPTS="-a localhost:6081 \
  63 + -T localhost:6082 \
  64 + -f /etc/varnish/default.vcl \
  65 + -S /etc/varnish/secret \
  66 + -s file,/var/lib/varnish/$INSTANCE/varnish_storage.bin,1G"
  67 +```
68 68  
69 69 Configure Pound:
70 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.
  71 +```
  72 +# cp /usr/share/noosfero/etc/pound.cfg /etc/pound/
  73 +```
74 74  
75   -Configure Pound to start at system initialization:
  75 +Edit `/etc/pound.cfg` and set the IP and domain of your server.
76 76  
77   -/etc/default/pound
  77 +Configure Pound to start at system initialization. At `/etc/default/pound`:
78 78 ------------------
79 79  
80   - startup=1
  80 +```
  81 +startup=1
  82 +```
81 83  
82   -Set Apache to only listen to localhost:
  84 +Set Apache to only listen to localhost, at `/etc/apache2/ports.conf`:
83 85  
84   -/etc/apache2/ports.conf
85   ------------------------
86   -
87   - Listen 127.0.0.1:8080
  86 +```
  87 +Listen 127.0.0.1:8080
  88 +```
88 89  
89 90 Restart the services:
90 91  
91   - $ sudo service apache2 restart
92   - $ sudo service varnish restart
  92 +```
  93 +$ sudo service apache2 restart
  94 +$ sudo service varnish restart
  95 +```
93 96  
94 97 Start pound:
95 98  
96   - $ sudo service pound start
97   -
98   -[1] http://www.apsis.ch/pound
  99 +```
  100 +$ sudo service pound start
  101 +```
99 102  
100   -Noosfero XMPP chat
101   -++++++++++++++++++
  103 +## Noosfero XMPP chat
102 104  
103 105 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:
  106 +and IP of your server in the /etc/hosts file, example
105 107  
106   -/etc/hosts
107   -----------
  108 +`/etc/hosts:`
108 109  
109   - 192.168.1.86 mydomain.example.com
  110 +```
  111 +192.168.1.86 mydomain.example.com
  112 +```
110 113  
111   -Also, it's recomended that you remove lines above from the file
  114 +Also, it's recomended that you remove the lines below from the file
112 115 `/etc/apache2/sites-enabled/noosfero`:
113 116  
114   - RewriteEngine On
115   - Include /usr/share/noosfero/util/chat/apache/xmpp.conf
  117 +```
  118 +RewriteEngine On
  119 +Include /usr/share/noosfero/util/chat/apache/xmpp.conf
  120 +```
... ...
INSTALL.locales.md 0 → 100644
... ... @@ -0,0 +1,29 @@
  1 +Using custom locales
  2 +====================
  3 +
  4 +Personalized translations go into the `config/custom_locales/` directory.
  5 +Custom locales can be identified by the rails environment, schema name in a
  6 +multitenancy setup or domain name until the first dot (e.g env1.coop.br for the
  7 +example below).
  8 +
  9 +Currently, the only filename prefix for the localization file which is
  10 +processed is "environment". For instance, a POT file would be called
  11 +"environment.pot".
  12 +
  13 +The structure of an environment named env1 with custom translations for both
  14 +Portuguese and Spanish and an environment named env2 with custom Russian
  15 +translation would be:
  16 +
  17 + config/
  18 + custom_locales/
  19 + env1/
  20 + environment.pot
  21 + pt/
  22 + environment.po
  23 + es/
  24 + environment.po
  25 + env2/
  26 + environment.pot
  27 + ru/
  28 + environment.po
  29 +
... ...
INSTALL.md
... ... @@ -186,8 +186,8 @@ Apache instalation
186 186  
187 187 # apt-get install apache2
188 188  
189   -Apache configuration
190   ---------------------
  189 +Configuration - noosfero at /
  190 +-----------------------------
191 191  
192 192 First you have to enable the following some apache modules:
193 193  
... ... @@ -257,6 +257,62 @@ Now restart your apache server (as root):
257 257  
258 258 # invoke-rc.d apache2 restart
259 259  
  260 +Configuration - noosfero at a /subdirectory
  261 +-------------------------------------------
  262 +
  263 +This section describes how to configure noosfero at a subdirectory, what is
  264 +specially useful when you want Noosfero to share a domain name with other
  265 +applications. For example you can host noosfero at yourdomain.com/social, a
  266 +webmail application at yourdomain.com/webmail, and have a static HTML website
  267 +at yourdomain.com/.
  268 +
  269 +**NOTE:** Some plugins might not work well with this setting. Before deploying
  270 +this setting, make sure you test that everything you need works properly with
  271 +it.
  272 +
  273 +The configuration is similar to the main configuration instructions, except for
  274 +the following points. In the description below, replace '/subdirectory' with
  275 +the actual subdirectory you want.
  276 +
  277 +1) add a `prefix: /subdirectory` line to your thin configuration file (thin.yml).
  278 +
  279 +1.1) remember to restart the noosfero application server whenever you make
  280 +changes to that configuration file.
  281 +
  282 + # service noosfero restart
  283 +
  284 +2) add a line saying `export RAILS_RELATIVE_URL_ROOT=/subdirectory` to
  285 +/etc/default/noosfero (you can create it with just this line if it does not
  286 +exist already).
  287 +
  288 +3) You should add the following apache configuration to an existing virtual
  289 +host (plus the `<Proxy balancer://noosfero>` section as displayed above):
  290 +
  291 +```
  292 +Alias /subdirectory /path/to/noosfero/public
  293 +<Directory "/path/to/noosfero/public">
  294 + Options FollowSymLinks
  295 + AllowOverride None
  296 + Order Allow,Deny
  297 + Allow from all
  298 +
  299 + Include /path/to/noosfero/etc/noosfero/apache/cache.conf
  300 +
  301 + RewriteEngine On
  302 + RewriteBase /subdirectory
  303 + # Rewrite index to check for static index.html
  304 + RewriteRule ^$ index.html [QSA]
  305 + # Rewrite to check for Rails cached page
  306 + RewriteRule ^([^.]+)$ $1.html [QSA]
  307 + RewriteCond %{REQUEST_FILENAME} !-f
  308 + RewriteRule ^(.*)$ http://localhost:3000%{REQUEST_URI} [P,QSA,L]
  309 +</Directory>
  310 +```
  311 +
  312 +3.1) remember to reload the apache server whenever any apache configuration
  313 +file changes.
  314 +
  315 + # sudo service apache2 reload
260 316  
261 317 Enabling exception notifications
262 318 ================================
... ...
MIGRATION_ISSUES
... ... @@ -1,41 +0,0 @@
1   -* ruby-get-text incmopatible with rails3. Maybe we can use it's gem
2   -
3   -* all js code is inside miscellaneous.js. Would be nice to refactor this
4   -
5   -* rails 2 uses prototype instead of jquery
6   -
7   -* config/environment.rb maybe still have some code that should be on the initializers
8   -
9   -* initializers session_store.rb inflections.rb... don't exist
10   -
11   -* rails gems version have to be forced on Gemfile or it will use incompatible pre3vious versions (3.1.3)
12   -
13   -* Sweepers are now natively supported on Rails 3. Would be nice to refactor it
14   -
15   -* On Rails 3 it is no more possible to add allowed tags to avoid scape. The html_safe initializer is an option.
16   -
17   -* error when call sqlite_extensiosn
18   -
19   -* error related to action_tracker
20   -
21   -* check FIXME's in script/quick-start
22   -
23   -* check FIXME's in Gemfile
24   -
25   -* Check the FIXME in config/routes.rb
26   -
27   -* rewrite conditional routing. See FIXME in lib/route_if.rb and re-implement using the Rails 3 mechanism - http://guides.rubyonrails.org/routing.html#advanced-constraints
28   -
29   -* check FIXME's in config/environment.rb
30   -
31   -* xss_terminate sucks. We should replace it with the builtin mechanism in Rails 3
32   -
33   -* instance_eval on Ruby 1.9 yields self, so lambdas that are passed to instance_eval and do not accept exactly 1 argument will blow up. See http://www.ruby-forum.com/topic/213313 ... search for instance_eval and fix where necessary. In special, most of the blocks still need fixing.
34   -
35   -* all instances of <% *_form_for ... %> must be changed to <%= instead of <%
36   -
37   -* all ActiveRecord models have to declare explicitly which attributes must be allowed for mass assignment with attr_accessible.
38   -
39   -* check if we need to update config/locales/*
40   -
41   -* check observe_field and labelled_form_for in app/helpers/application_helper.rb
Rakefile
... ... @@ -6,3 +6,13 @@
6 6 require File.expand_path('../config/application', __FILE__)
7 7  
8 8 Noosfero::Application.load_tasks
  9 +
  10 +[
  11 + "baseplugins/*/{tasks,lib/tasks,rails/tasks}/**/*.rake",
  12 + "config/plugins/*/{tasks,lib/tasks,rails/tasks}/**/*.rake",
  13 + "config/plugins/*/vendor/plugins/*/{tasks,lib/tasks,rails/tasks}/**/*.rake",
  14 +].map do |pattern|
  15 + Dir.glob(pattern).sort
  16 +end.flatten.each do |taskfile|
  17 + load taskfile
  18 +end
... ...
Vagrantfile
... ... @@ -3,7 +3,7 @@
3 3  
4 4 VAGRANTFILE_API_VERSION = "2"
5 5 Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
6   - config.vm.box = "debian-wheezy"
  6 + config.vm.box = ENV.fetch('VAGRANT_BOX', "debian-wheezy")
7 7 config.vm.network :forwarded_port, host: 3000, guest: 3000
8 8 config.vm.provision :shell do |shell|
9 9 shell.inline = 'su vagrant -c /vagrant/script/vagrant'
... ...
app/controllers/admin/admin_panel_controller.rb
... ... @@ -71,4 +71,22 @@ class AdminPanelController &lt; AdminController
71 71 end
72 72 end
73 73 end
  74 +
  75 + def manage_organizations_status
  76 + scope = environment.organizations
  77 + @filter = params[:filter] || 'any'
  78 + @title = "Organization profiles"
  79 + @title = @title+" - "+@filter if @filter != 'any'
  80 +
  81 + if @filter == 'enabled'
  82 + scope = scope.visible
  83 + elsif @filter == 'disabled'
  84 + scope = scope.disabled
  85 + end
  86 +
  87 + scope = scope.order('name ASC')
  88 +
  89 + @q = params[:q]
  90 + @collection = find_by_contents(:organizations, scope, @q, {:per_page => 10, :page => params[:npage]})[:results]
  91 + end
74 92 end
... ...
app/controllers/admin/categories_controller.rb
... ... @@ -45,9 +45,11 @@ class CategoriesController &lt; AdminController
45 45 if request.post?
46 46 @category.update_attributes!(params[:category])
47 47 @saved = true
  48 + session[:notice] = _("Category %s saved." % @category.name)
48 49 redirect_to :action => 'index'
49 50 end
50 51 rescue Exception => e
  52 + session[:notice] = _('Could not save category.')
51 53 render :action => 'edit'
52 54 end
53 55 end
... ...
app/controllers/admin/environment_design_controller.rb
... ... @@ -3,9 +3,7 @@ class EnvironmentDesignController &lt; BoxOrganizerController
3 3 protect 'edit_environment_design', :environment
4 4  
5 5 def available_blocks
6   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
7   - # the Noosfero core soon, see ActionItem3045
8   - @available_blocks ||= [ ArticleBlock, LoginBlock, EnvironmentStatisticsBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock, RawHTMLBlock, TagsBlock ]
  6 + @available_blocks ||= [ ArticleBlock, LoginBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock, RawHTMLBlock, TagsBlock ]
9 7 @available_blocks += plugins.dispatch(:extra_blocks, :type => Environment)
10 8 end
11 9  
... ...
app/controllers/admin/features_controller.rb
... ... @@ -51,4 +51,10 @@ class FeaturesController &lt; AdminController
51 51 redirect_to :action => 'manage_fields'
52 52 end
53 53  
  54 + def search_members
  55 + arg = params[:q].downcase
  56 + result = environment.people.find(:all, :conditions => ['LOWER(name) LIKE ? OR identifier LIKE ?', "%#{arg}%", "%#{arg}%"])
  57 + render :text => prepare_to_token_input(result).to_json
  58 + end
  59 +
54 60 end
... ...
app/controllers/admin/plugin_admin_controller.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +class PluginAdminController < AdminController
  2 +
  3 + protect 'edit_environment_features', :environment
  4 +
  5 +end
... ...
app/controllers/admin/region_validators_controller.rb
... ... @@ -33,7 +33,7 @@ class RegionValidatorsController &lt; AdminController
33 33 def load_region_and_search
34 34 @region = environment.regions.find(params[:id])
35 35 if params[:search]
36   - @search = find_by_contents(:organizations, Organization, params[:search])[:results].reject {|item| @region.validator_ids.include?(item.id) }
  36 + @search = find_by_contents(:organizations, environment, Organization, params[:search])[:results].reject {|item| @region.validator_ids.include?(item.id) }
37 37 end
38 38 end
39 39  
... ...
app/controllers/admin/templates_controller.rb
... ... @@ -40,8 +40,67 @@ class TemplatesController &lt; AdminController
40 40 end
41 41 end
42 42  
  43 + def set_community_as_default
  44 + begin
  45 + community = environment.communities.find(params[:template_id])
  46 + rescue ActiveRecord::RecordNotFound
  47 + message = _('Community not found. The template could no be changed.')
  48 + community = nil
  49 + end
  50 +
  51 + message = _('%s defined as default') % community.name if set_as_default(community)
  52 + session[:notice] = message
  53 +
  54 + redirect_to :action => 'index'
  55 + end
  56 +
  57 + def set_person_as_default
  58 + begin
  59 + person = environment.people.find(params[:template_id])
  60 + rescue ActiveRecord::RecordNotFound
  61 + message = _('Person not found. The template could no be changed.')
  62 + person = nil
  63 + end
  64 +
  65 + message = _('%s defined as default') % person.name if set_as_default(person)
  66 + session[:notice] = message
  67 +
  68 + redirect_to :action => 'index'
  69 + end
  70 +
  71 + def set_enterprise_as_default
  72 + begin
  73 + enterprise = environment.enterprises.find(params[:template_id])
  74 + rescue ActiveRecord::RecordNotFound
  75 + message = _('Enterprise not found. The template could no be changed.')
  76 + enterprise = nil
  77 + end
  78 +
  79 + message = _('%s defined as default') % enterprise.name if set_as_default(enterprise)
  80 + session[:notice] = message
  81 +
  82 + redirect_to :action => 'index'
  83 + end
  84 +
43 85 private
44 86  
  87 + def set_as_default(obj)
  88 + return nil if obj.nil?
  89 + case obj.class.name
  90 + when 'Community' then
  91 + environment.community_default_template = obj
  92 + environment.save!
  93 + when 'Person' then
  94 + environment.person_default_template = obj
  95 + environment.save!
  96 + when 'Enterprise' then
  97 + environment.enterprise_default_template = obj
  98 + environment.save!
  99 + else
  100 + nil
  101 + end
  102 + end
  103 +
45 104 def create_organization_template(klass)
46 105 identifier = params[:name].to_slug
47 106 template = klass.new(:name => params[:name], :identifier => identifier, :is_template => true)
... ...
app/controllers/admin/users_controller.rb
... ... @@ -18,7 +18,7 @@ class UsersController &lt; AdminController
18 18 end
19 19 scope = scope.order('name ASC')
20 20 @q = params[:q]
21   - @collection = find_by_contents(:people, scope, @q, {:per_page => per_page, :page => params[:npage]})[:results]
  21 + @collection = find_by_contents(:people, environment, scope, @q, {:per_page => per_page, :page => params[:npage]})[:results]
22 22 end
23 23  
24 24 def set_admin_role
... ...
app/controllers/application_controller.rb
... ... @@ -7,6 +7,12 @@ class ApplicationController &lt; ActionController::Base
7 7 before_filter :detect_stuff_by_domain
8 8 before_filter :init_noosfero_plugins
9 9 before_filter :allow_cross_domain_access
  10 + before_filter :login_required, :if => :private_environment?
  11 + before_filter :verify_members_whitelist, :if => [:private_environment?, :user]
  12 +
  13 + def verify_members_whitelist
  14 + render_access_denied unless user.is_admin? || environment.in_whitelist?(user)
  15 + end
10 16  
11 17 after_filter :set_csrf_cookie
12 18  
... ... @@ -34,7 +40,7 @@ class ApplicationController &lt; ActionController::Base
34 40  
35 41 theme_layout = theme_option(:layout)
36 42 if theme_layout
37   - theme_view_file('layouts/'+theme_layout) || theme_layout
  43 + (theme_view_file('layouts/'+theme_layout) || theme_layout).to_s
38 44 else
39 45 'application'
40 46 end
... ... @@ -121,6 +127,9 @@ class ApplicationController &lt; ActionController::Base
121 127  
122 128 # TODO: move this logic somewhere else (Domain class?)
123 129 def detect_stuff_by_domain
  130 + # Sets text domain based on request host for custom internationalization
  131 + FastGettext.text_domain = Domain.custom_locale(request.host)
  132 +
124 133 @domain = Domain.find_by_name(request.host)
125 134 if @domain.nil?
126 135 @environment = Environment.default
... ... @@ -174,17 +183,19 @@ class ApplicationController &lt; ActionController::Base
174 183 end
175 184 end
176 185  
177   - def find_by_contents(asset, scope, query, paginate_options={:page => 1}, options={})
178   - plugins.dispatch_first(:find_by_contents, asset, scope, query, paginate_options, options) ||
179   - fallback_find_by_contents(asset, scope, query, paginate_options, options)
180   - end
  186 + include SearchTermHelper
181 187  
182   - private
  188 + def find_by_contents(asset, context, scope, query, paginate_options={:page => 1}, options={})
  189 + search = plugins.dispatch_first(:find_by_contents, asset, scope, query, paginate_options, options)
  190 + register_search_term(query, scope.count, search[:results].count, context, asset)
  191 + search
  192 + end
183 193  
184   - def fallback_find_by_contents(asset, scope, query, paginate_options, options)
185   - scope = scope.like_search(query) unless query.blank?
186   - scope = scope.send(options[:filter]) unless options[:filter].blank?
187   - {:results => scope.paginate(paginate_options)}
  194 + def find_suggestions(query, context, asset, options={})
  195 + plugins.dispatch_first(:find_suggestions, query, context, asset, options)
188 196 end
189 197  
  198 + def private_environment?
  199 + @environment.enabled?(:restrict_to_members)
  200 + end
190 201 end
... ...
app/controllers/my_profile/cms_controller.rb
... ... @@ -4,6 +4,12 @@ class CmsController &lt; MyProfileController
4 4  
5 5 include ArticleHelper
6 6  
  7 + def search_tags
  8 + arg = params[:term].downcase
  9 + result = ActsAsTaggableOn::Tag.find(:all, :conditions => ['LOWER(name) LIKE ?', "%#{arg}%"])
  10 + render :text => prepare_to_token_input_by_label(result).to_json, :content_type => 'application/json'
  11 + end
  12 +
7 13 def self.protect_if(*args)
8 14 before_filter(*args) do |c|
9 15 user, profile = c.send(:user), c.send(:profile)
... ... @@ -17,6 +23,9 @@ class CmsController &lt; MyProfileController
17 23 end
18 24  
19 25 before_filter :login_required, :except => [:suggest_an_article]
  26 + before_filter :load_recent_files, :only => [:new, :edit]
  27 +
  28 + helper_method :file_types
20 29  
21 30 protect_if :only => :upload_files do |c, user, profile|
22 31 article_id = c.params[:parent_id]
... ... @@ -24,7 +33,7 @@ class CmsController &lt; MyProfileController
24 33 (user && (user.has_permission?('post_content', profile) || user.has_permission?('publish_content', profile)))
25 34 end
26 35  
27   - protect_if :except => [:suggest_an_article, :set_home_page, :edit, :destroy, :publish, :upload_files, :new] do |c, user, profile|
  36 + protect_if :except => [:suggest_an_article, :set_home_page, :edit, :destroy, :publish, :publish_on_portal_community, :publish_on_communities, :search_communities_to_publish, :upload_files, :new] do |c, user, profile|
28 37 user && (user.has_permission?('post_content', profile) || user.has_permission?('publish_content', profile))
29 38 end
30 39  
... ... @@ -34,7 +43,7 @@ class CmsController &lt; MyProfileController
34 43 (user && (user.has_permission?('post_content', profile) || user.has_permission?('publish_content', profile)))
35 44 end
36 45  
37   - protect_if :only => [:destroy, :publish] do |c, user, profile|
  46 + protect_if :only => :destroy do |c, user, profile|
38 47 profile.articles.find(c.params[:id]).allow_post_content?(user)
39 48 end
40 49  
... ... @@ -111,7 +120,7 @@ class CmsController &lt; MyProfileController
111 120 @success_back_to = params[:success_back_to]
112 121 # user must choose an article type first
113 122  
114   - @parent = profile.articles.find(params[:parent_id]) if params && params[:parent_id]
  123 + @parent = profile.articles.find(params[:parent_id]) if params && params[:parent_id].present?
115 124 record_coming
116 125 @type = params[:type]
117 126 if @type.blank?
... ... @@ -157,7 +166,10 @@ class CmsController &lt; MyProfileController
157 166 if continue
158 167 redirect_to :action => 'edit', :id => @article
159 168 else
160   - success_redirect
  169 + respond_to do |format|
  170 + format.html { success_redirect }
  171 + format.json { render :text => {:id => @article.id, :full_name => profile.identifier + '/' + @article.full_name}.to_json }
  172 + end
161 173 end
162 174 return
163 175 end
... ... @@ -168,6 +180,8 @@ class CmsController &lt; MyProfileController
168 180  
169 181 post_only :set_home_page
170 182 def set_home_page
  183 + return render_access_denied unless user.can_change_homepage?
  184 +
171 185 article = params[:id].nil? ? nil : profile.articles.find(params[:id])
172 186 profile.update_attribute(:home_page, article)
173 187  
... ... @@ -206,6 +220,7 @@ class CmsController &lt; MyProfileController
206 220 if @errors.any?
207 221 render :action => 'upload_files', :parent_id => @parent_id
208 222 else
  223 + session[:notice] = _('File(s) successfully uploaded')
209 224 if @back_to
210 225 redirect_to @back_to
211 226 elsif @parent
... ... @@ -221,7 +236,7 @@ class CmsController &lt; MyProfileController
221 236 @article = profile.articles.find(params[:id])
222 237 if request.post?
223 238 @article.destroy
224   - session[:notice] = _("\"#{@article.name}\" was removed.")
  239 + session[:notice] = _("\"%s\" was removed." % @article.name)
225 240 referer = Rails.application.routes.recognize_path URI.parse(request.referer).path rescue nil
226 241 if referer and referer[:controller] == 'cms' and referer[:action] != 'edit'
227 242 redirect_to referer
... ... @@ -247,28 +262,53 @@ class CmsController &lt; MyProfileController
247 262 render :template => 'shared/update_categories', :locals => { :category => @current_category, :object_name => 'article' }
248 263 end
249 264  
  265 + def search_communities_to_publish
  266 + render :text => find_by_contents(:profiles, environment, user.memberships, params['q'], {:page => 1}, {:fields => ['name']})[:results].map {|community| {:id => community.id, :name => community.name} }.to_json
  267 + end
  268 +
250 269 def publish
251 270 @article = profile.articles.find(params[:id])
252 271 record_coming
253   - @groups = profile.memberships - [profile]
254   - @marked_groups = []
255   - groups_ids = profile.memberships.map{|m|m.id.to_s}
256   - @marked_groups = params[:marked_groups].map do |key, item|
257   - if groups_ids.include?(item[:group_id])
258   - item.merge :group => Profile.find(item.delete(:group_id))
  272 + @failed = {}
  273 + if request.post?
  274 + article_name = params[:name]
  275 + task = ApproveArticle.create!(:article => @article, :name => article_name, :target => user, :requestor => user)
  276 + begin
  277 + task.finish
  278 + rescue Exception => ex
  279 + @failed[ex.message] ? @failed[ex.message] << @article.name : @failed[ex.message] = [@article.name]
  280 + task.cancel
259 281 end
260   - end.compact unless params[:marked_groups].nil?
  282 + if @failed.blank?
  283 + session[:notice] = _("Your publish request was sent successfully")
  284 + if @back_to
  285 + redirect_to @back_to
  286 + else
  287 + redirect_to @article.view_url
  288 + end
  289 + end
  290 + end
  291 + end
  292 +
  293 + def publish_on_communities
261 294 if request.post?
  295 + @back_to = params[:back_to]
  296 + @article = profile.articles.find(params[:id])
262 297 @failed = {}
  298 + article_name = params[:name]
  299 + params_marked = (params['q'] || '').split(',').select { |marked| user.memberships.map(&:id).include? marked.to_i }
  300 + @marked_groups = Profile.find(params_marked)
263 301 if @marked_groups.empty?
  302 + redirect_to @back_to
264 303 return session[:notice] = _("Select some group to publish your article")
265 304 end
266 305 @marked_groups.each do |item|
267   - task = ApproveArticle.create!(:article => @article, :name => item[:name], :target => item[:group], :requestor => profile)
  306 + task = ApproveArticle.create!(:article => @article, :name => article_name, :target => item, :requestor => user)
268 307 begin
269   - task.finish unless item[:group].moderated_articles?
  308 + task.finish unless item.moderated_articles?
270 309 rescue Exception => ex
271   - @failed[ex.message] ? @failed[ex.message] << item[:group].name : @failed[ex.message] = [item[:group].name]
  310 + @failed[ex.message] ? @failed[ex.message] << item.name : @failed[ex.message] = [item.name]
  311 + task.cancel
272 312 end
273 313 end
274 314 if @failed.blank?
... ... @@ -278,23 +318,27 @@ class CmsController &lt; MyProfileController
278 318 else
279 319 redirect_to @article.view_url
280 320 end
  321 + else
  322 + session[:notice] = _("Some of your publish requests couldn't be sent.")
  323 + render :action => 'publish'
281 324 end
282 325 end
283 326 end
284 327  
285 328 def publish_on_portal_community
286   - @article = profile.articles.find(params[:id])
287 329 if request.post?
288   - if environment.portal_community
  330 + @article = profile.articles.find(params[:id])
  331 + if environment.portal_enabled
289 332 task = ApproveArticle.create!(:article => @article, :name => params[:name], :target => environment.portal_community, :requestor => user)
290 333 begin
291 334 task.finish unless environment.portal_community.moderated_articles?
292   - flash[:notice] = _("Your publish request was sent successfully")
  335 + session[:notice] = _("Your publish request was sent successfully")
293 336 rescue
294   - flash[:error] = _("Your publish request couldn't be sent.")
  337 + session[:notice] = _("Your publish request couldn't be sent.")
  338 + task.cancel
295 339 end
296 340 else
297   - flash[:notice] = _("There is no portal community to publish your article.")
  341 + session[:notice] = _("There is no portal community to publish your article.")
298 342 end
299 343  
300 344 if @back_to
... ... @@ -322,7 +366,7 @@ class CmsController &lt; MyProfileController
322 366  
323 367 def search
324 368 query = params[:q]
325   - results = find_by_contents(:uploaded_files, profile.files.published, query)[:results]
  369 + results = find_by_contents(:uploaded_files, profile, profile.files.published, query)[:results]
326 370 render :text => article_list_to_json(results), :content_type => 'application/json'
327 371 end
328 372  
... ... @@ -333,15 +377,26 @@ class CmsController &lt; MyProfileController
333 377 end
334 378  
335 379 def media_upload
336   - files_uploaded = []
337 380 parent = check_parent(params[:parent_id])
338   - files = [:file1,:file2, :file3].map { |f| params[f] }.compact
339 381 if request.post?
340   - files.each do |file|
341   - files_uploaded << UploadedFile.create(:uploaded_data => file, :profile => profile, :parent => parent) unless file == ''
  382 + begin
  383 + @file = UploadedFile.create!(:uploaded_data => params[:file], :profile => profile, :parent => parent) unless params[:file] == ''
  384 + @file = FilePresenter.for(@file)
  385 + rescue Exception => exception
  386 + render :text => exception.to_s, :status => :bad_request
342 387 end
343 388 end
344   - render :text => article_list_to_json(files_uploaded), :content_type => 'text/plain'
  389 + end
  390 +
  391 + def published_media_items
  392 + load_recent_files(params[:parent_id], params[:q])
  393 + render :partial => 'published_media_items'
  394 + end
  395 +
  396 + def view_all_media
  397 + paginate_options = {:page => params[:page].blank? ? 1 : params[:page] }
  398 + @key = params[:key].to_sym
  399 + load_recent_files(params[:parent_id], params[:q], paginate_options)
345 400 end
346 401  
347 402 protected
... ... @@ -436,4 +491,36 @@ class CmsController &lt; MyProfileController
436 491 end
437 492 end
438 493  
  494 + def file_types
  495 + {:images => _('Images'), :generics => _('Files')}
  496 + end
  497 +
  498 + def load_recent_files(parent_id = nil, q = nil, paginate_options = {:page => 1, :per_page => 6})
  499 + #TODO Since we only have special support for images, I'm limiting myself to
  500 + # consider generic files as non-images. In the future, with more supported
  501 + # file types we'll need to have a smart way to fetch from the database
  502 + # scopes of each supported type as well as the non-supported types as a
  503 + # whole.
  504 + @recent_files = {}
  505 +
  506 + parent = parent_id.present? ? profile.articles.find(parent_id) : nil
  507 + if parent.present?
  508 + files = parent.children.files
  509 + else
  510 + files = profile.files
  511 + end
  512 +
  513 + files = files.reorder('created_at DESC')
  514 + images = files.images
  515 + generics = files.no_images
  516 +
  517 + if q.present?
  518 + @recent_files[:images] = find_by_contents(:images, profile, images, q, paginate_options)[:results]
  519 + @recent_files[:generics] = find_by_contents(:generics, profile, generics, q, paginate_options)[:results]
  520 + else
  521 + @recent_files[:images] = images.paginate(paginate_options)
  522 + @recent_files[:generics] = generics.paginate(paginate_options)
  523 + end
  524 + end
  525 +
439 526 end
... ...
app/controllers/my_profile/friends_controller.rb 100644 → 100755
... ... @@ -3,6 +3,7 @@ class FriendsController &lt; MyProfileController
3 3 protect 'manage_friends', :profile
4 4  
5 5 def index
  6 + @suggestions = profile.profile_suggestions.of_person.enabled.includes(:suggestion).limit(per_page)
6 7 if is_cache_expired?(profile.manage_friends_cache_key(params))
7 8 @friends = profile.friends.paginate(:per_page => per_page, :page => params[:npage])
8 9 end
... ... @@ -16,11 +17,35 @@ class FriendsController &lt; MyProfileController
16 17 end
17 18 end
18 19  
  20 + def suggest
  21 + @suggestions = profile.profile_suggestions.of_person.enabled.includes(:suggestion).limit(per_page)
  22 + end
  23 +
  24 + def remove_suggestion
  25 + @person = profile.suggested_people.find_by_identifier(params[:id])
  26 + redirect_to :action => 'suggest' unless @person
  27 + if @person && request.post?
  28 + profile.remove_suggestion(@person)
  29 + @suggestions = profile.profile_suggestions.of_person.enabled.includes(:suggestion).limit(per_page)
  30 + render :partial => 'shared/profile_suggestions_list', :locals => { :suggestions => @suggestions, :collection => :friends_suggestions, :per_page => params[:per_page] || per_page }
  31 + end
  32 + end
  33 +
  34 + def connections
  35 + @suggestion = profile.profile_suggestions.of_person.enabled.find_by_suggestion_id(params[:id])
  36 + if @suggestion
  37 + @tags = @suggestion.tag_connections
  38 + @profiles = @suggestion.profile_connections
  39 + else
  40 + redirect_to :action => 'suggest'
  41 + end
  42 + end
  43 +
19 44 protected
20 45  
21 46 class << self
22 47 def per_page
23   - 10
  48 + 12
24 49 end
25 50 end
26 51 def per_page
... ...
app/controllers/my_profile/memberships_controller.rb
... ... @@ -20,9 +20,54 @@ class MembershipsController &lt; MyProfileController
20 20 @community.environment = environment
21 21 @back_to = params[:back_to] || url_for(:action => 'index')
22 22 if request.post? && @community.valid?
23   - @community = Community.create_after_moderation(user, params[:community].merge({:environment => environment}))
24   - redirect_to @back_to
  23 + begin
  24 + # Community was created
  25 + @community = Community.create_after_moderation(user, params[:community].merge({:environment => environment}))
  26 + @community.reload
  27 + redirect_to :action => 'welcome', :community_id => @community.id, :back_to => @back_to
  28 + rescue ActiveRecord::RecordNotFound
  29 + # Community pending approval
  30 + session[:notice] = _('Your new community creation request will be evaluated by an administrator. You will be notified.')
  31 + redirect_to @back_to
  32 + end
25 33 return
26 34 end
27 35 end
  36 +
  37 + def welcome
  38 + @community = Community.find(params[:community_id])
  39 + @back_to = params[:back_to]
  40 + end
  41 +
  42 + def suggest
  43 + @suggestions = profile.profile_suggestions.of_community.enabled.includes(:suggestion).limit(per_page)
  44 + end
  45 +
  46 + def remove_suggestion
  47 + @community = profile.suggested_communities.find_by_identifier(params[:id])
  48 + custom_per_page = params[:per_page] || per_page
  49 + redirect_to :action => 'suggest' unless @community
  50 + if @community && request.post?
  51 + profile.remove_suggestion(@community)
  52 + @suggestions = profile.profile_suggestions.of_community.enabled.includes(:suggestion).limit(custom_per_page)
  53 + render :partial => 'shared/profile_suggestions_list', :locals => { :suggestions => @suggestions, :collection => :communities_suggestions, :per_page => custom_per_page}
  54 + end
  55 + end
  56 +
  57 + def connections
  58 + @suggestion = profile.profile_suggestions.of_community.enabled.find_by_suggestion_id(params[:id])
  59 + if @suggestion
  60 + @tags = @suggestion.tag_connections
  61 + @profiles = @suggestion.profile_connections
  62 + else
  63 + redirect_to :action => 'suggest'
  64 + end
  65 + end
  66 +
  67 + protected
  68 +
  69 + def per_page
  70 + 12
  71 + end
  72 +
28 73 end
... ...
app/controllers/my_profile/profile_design_controller.rb
... ... @@ -3,7 +3,16 @@ class ProfileDesignController &lt; BoxOrganizerController
3 3 needs_profile
4 4  
5 5 protect 'edit_profile_design', :profile
6   -
  6 +
  7 + before_filter :protect_fixed_block, :only => [:save, :move_block]
  8 +
  9 + def protect_fixed_block
  10 + block = boxes_holder.blocks.find(params[:id].gsub(/^block-/, ''))
  11 + if block.fixed && !current_person.is_admin?
  12 + render_access_denied
  13 + end
  14 + end
  15 +
7 16 def available_blocks
8 17 blocks = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock, FeedReaderBlock, ProfileImageBlock, LocationBlock, SlideshowBlock, ProfileSearchBlock, HighlightsBlock ]
9 18  
... ...
app/controllers/my_profile/profile_editor_controller.rb
... ... @@ -3,6 +3,10 @@ class ProfileEditorController &lt; MyProfileController
3 3 protect 'edit_profile', :profile, :except => [:destroy_profile]
4 4 protect 'destroy_profile', :profile, :only => [:destroy_profile]
5 5  
  6 + before_filter :access_welcome_page, :only => [:welcome_page]
  7 + before_filter :back_to
  8 + helper_method :has_welcome_page
  9 +
6 10 def index
7 11 @pending_tasks = Task.to(profile).pending.without_spam.select{|i| user.has_permission?(i.permission, profile)}
8 12 end
... ... @@ -16,14 +20,16 @@ class ProfileEditorController &lt; MyProfileController
16 20 if request.post?
17 21 params[:profile_data][:fields_privacy] ||= {} if profile.person? && params[:profile_data].is_a?(Hash)
18 22 Profile.transaction do
19   - Image.transaction do
20   - if @profile_data.update_attributes(params[:profile_data])
21   - redirect_to :action => 'index', :profile => profile.identifier
22   - else
23   - profile.identifier = params[:profile] if profile.identifier.blank?
  23 + Image.transaction do
  24 + begin
  25 + @plugins.dispatch(:profile_editor_transaction_extras)
  26 + @profile_data.update_attributes!(params[:profile_data])
  27 + redirect_to :action => 'index', :profile => profile.identifier
  28 + rescue Exception => ex
  29 + profile.identifier = params[:profile] if profile.identifier.blank?
  30 + end
24 31 end
25 32 end
26   - end
27 33 end
28 34 end
29 35  
... ... @@ -72,10 +78,81 @@ class ProfileEditorController &lt; MyProfileController
72 78 if request.post?
73 79 if @profile.destroy
74 80 session[:notice] = _('The profile was deleted.')
75   - redirect_to :controller => 'home'
  81 + if(params[:return_to])
  82 + redirect_to params[:return_to]
  83 + else
  84 + redirect_to :controller => 'home'
  85 + end
76 86 else
77 87 session[:notice] = _('Could not delete profile')
78 88 end
79 89 end
80 90 end
  91 +
  92 + def welcome_page
  93 + @welcome_page = profile.welcome_page || TinyMceArticle.new(:name => 'Welcome Page', :profile => profile, :published => false)
  94 + if request.post?
  95 + begin
  96 + @welcome_page.update_attributes!(params[:welcome_page])
  97 + profile.welcome_page = @welcome_page
  98 + profile.save!
  99 + session[:notice] = _('Welcome page saved successfully.')
  100 + redirect_to :action => 'index'
  101 + rescue Exception => exception
  102 + session[:notice] = _('Welcome page could not be saved.')
  103 + end
  104 + end
  105 + end
  106 +
  107 + def deactivate_profile
  108 + if environment.admins.include?(current_person)
  109 + profile = environment.profiles.find(params[:id])
  110 + if profile.disable
  111 + profile.save
  112 + session[:notice] = _("The profile '#{profile.name}' was deactivated.")
  113 + else
  114 + session[:notice] = _('Could not deactivate profile.')
  115 + end
  116 + end
  117 +
  118 + redirect_to_previous_location
  119 + end
  120 +
  121 + def activate_profile
  122 + if environment.admins.include?(current_person)
  123 + profile = environment.profiles.find(params[:id])
  124 +
  125 + if profile.enable
  126 + session[:notice] = _("The profile '#{profile.name}' was activated.")
  127 + else
  128 + session[:notice] = _('Could not activate the profile.')
  129 + end
  130 + end
  131 +
  132 + redirect_to_previous_location
  133 + end
  134 +
  135 + protected
  136 +
  137 + def redirect_to_previous_location
  138 + redirect_to @back_to
  139 + end
  140 +
  141 + #TODO Consider using this as a general controller feature to be available on every action.
  142 + def back_to
  143 + @back_to = params[:back_to] || request.referer || "/"
  144 + end
  145 +
  146 + private
  147 +
  148 + def has_welcome_page
  149 + profile.is_template
  150 + end
  151 +
  152 + def access_welcome_page
  153 + unless has_welcome_page
  154 + render_access_denied
  155 + end
  156 + end
  157 +
81 158 end
... ...
app/controllers/my_profile/profile_members_controller.rb
... ... @@ -20,7 +20,7 @@ class ProfileMembersController &lt; MyProfileController
20 20 redirect_to :action => :last_admin
21 21 elsif @person.define_roles(@roles, profile)
22 22 session[:notice] = _('Roles successfuly updated')
23   - redirect_to :controller => 'profile_editor'
  23 + redirect_to :action => 'index'
24 24 else
25 25 session[:notice] = _('Couldn\'t change the roles')
26 26 redirect_to :action => 'index'
... ...
app/controllers/my_profile/tasks_controller.rb
... ... @@ -4,6 +4,7 @@ class TasksController &lt; MyProfileController
4 4  
5 5 def index
6 6 @filter = params[:filter_type].blank? ? nil : params[:filter_type]
  7 + @task_types = Task.pending_types_for(profile)
7 8 @tasks = Task.to(profile).without_spam.pending.of(@filter).order_by('created_at', 'asc').paginate(:per_page => Task.per_page, :page => params[:page])
8 9 @failed = params ? params[:failed] : {}
9 10 end
... ...
app/controllers/public/account_controller.rb
... ... @@ -15,11 +15,23 @@ class AccountController &lt; ApplicationController
15 15  
16 16 def activate
17 17 @user = User.find_by_activation_code(params[:activation_code]) if params[:activation_code]
18   - if @user and @user.activate
19   - @message = _("Your account has been activated, now you can log in!")
20   - check_redirection
21   - session[:join] = params[:join] unless params[:join].blank?
22   - render :action => 'login', :userlogin => @user.login
  18 + if @user
  19 + unless @user.environment.enabled?('admin_must_approve_new_users')
  20 + if @user.activate
  21 + @message = _("Your account has been activated, now you can log in!")
  22 + check_redirection
  23 + session[:join] = params[:join] unless params[:join].blank?
  24 + render :action => 'login', :userlogin => @user.login
  25 + end
  26 + else
  27 + if @user.create_moderate_task
  28 + session[:notice] = _('Thanks for registering. The administrators were notified.')
  29 + @register_pending = true
  30 + @user.activation_code = nil
  31 + @user.save!
  32 + redirect_to :controller => :home
  33 + end
  34 + end
23 35 else
24 36 session[:notice] = _("It looks like you're trying to activate an account. Perhaps have already activated this account?")
25 37 redirect_to :controller => :home
... ... @@ -70,10 +82,12 @@ class AccountController &lt; ApplicationController
70 82 if @plugins.dispatch(:allow_user_registration).include?(false)
71 83 redirect_back_or_default(:controller => 'home')
72 84 session[:notice] = _("This environment doesn't allow user registration.")
  85 + return
73 86 end
74 87  
75 88 store_location(request.referer) unless params[:return_to] or session[:return_to]
76 89  
  90 + # Tranforming to boolean
77 91 @block_bot = !!session[:may_be_a_bot]
78 92 @invitation_code = params[:invitation_code]
79 93 begin
... ... @@ -85,6 +99,7 @@ class AccountController &lt; ApplicationController
85 99 @user.return_to = session[:return_to]
86 100 @person = Person.new(params[:profile_data])
87 101 @person.environment = @user.environment
  102 +
88 103 if request.post?
89 104 if may_be_a_bot
90 105 set_signup_start_time_for_now
... ... @@ -103,12 +118,21 @@ class AccountController &lt; ApplicationController
103 118 invitation.update_attributes!({:friend => @user.person})
104 119 invitation.finish
105 120 end
  121 +
  122 + unless params[:file].nil?
  123 + image = Image::new :uploaded_data=> params[:file][:image]
  124 +
  125 + @user.person.image = image
  126 + @user.person.save
  127 + end
  128 +
106 129 if @user.activated?
107 130 self.current_user = @user
108 131 check_join_in_community(@user)
109 132 go_to_signup_initial_page
110 133 else
111   - @register_pending = true
  134 + redirect_to :controller => :home, :action => :welcome, :template_id => (@user.person.template && @user.person.template.id)
  135 + session[:notice] = _('Thanks for registering!')
112 136 end
113 137 end
114 138 end
... ... @@ -171,7 +195,7 @@ class AccountController &lt; ApplicationController
171 195 else
172 196 @change_password.errors[:base] << _('Could not find any user with %s equal to "%s".') % [fields_label, params[:value]]
173 197 end
174   - rescue ActiveRecord::RecordInvald
  198 + rescue ActiveRecord::RecordInvalid
175 199 @change_password.errors[:base] << _('Could not perform password recovery for the user.')
176 200 end
177 201 end
... ... @@ -439,6 +463,8 @@ class AccountController &lt; ApplicationController
439 463 redirect_to user.url
440 464 when 'user_control_panel'
441 465 redirect_to user.admin_url
  466 + when 'welcome_page'
  467 + redirect_to :controller => :home, :action => :welcome, :template_id => (user.template && user.template.id)
442 468 else
443 469 redirect_back_or_default(default)
444 470 end
... ...
app/controllers/public/chat_controller.rb
... ... @@ -19,9 +19,13 @@ class ChatController &lt; PublicController
19 19 def avatar
20 20 profile = environment.profiles.find_by_identifier(params[:id])
21 21 filename, mimetype = profile_icon(profile, :minor, true)
22   - data = File.read(File.join(Rails.root, 'public', filename))
23   - render :text => data, :layout => false, :content_type => mimetype
24   - expires_in 24.hours
  22 + if filename =~ /^(https?:)?\/\//
  23 + redirect_to filename
  24 + else
  25 + data = File.read(File.join(Rails.root, 'public', filename))
  26 + render :text => data, :layout => false, :content_type => mimetype
  27 + expires_in 24.hours
  28 + end
25 29 end
26 30  
27 31 def index
... ...
app/controllers/public/contact_controller.rb
1 1 class ContactController < PublicController
2 2  
3   - before_filter :login_required
4   -
5 3 needs_profile
6 4  
7 5 def new
8   - @contact
  6 + @contact = build_contact
9 7 if request.post? && params[:confirm] == 'true'
10   - @contact = user.build_contact(profile, params[:contact])
11 8 @contact.city = (!params[:city].blank? && City.exists?(params[:city])) ? City.find(params[:city]).name : nil
12 9 @contact.state = (!params[:state].blank? && State.exists?(params[:state])) ? State.find(params[:state]).name : nil
13 10 if @contact.deliver
... ... @@ -16,8 +13,17 @@ class ContactController &lt; PublicController
16 13 else
17 14 session[:notice] = _('Contact not sent')
18 15 end
  16 + end
  17 + end
  18 +
  19 + protected
  20 +
  21 + def build_contact
  22 + params[:contact] ||= {}
  23 + if logged_in?
  24 + user.build_contact profile, params[:contact]
19 25 else
20   - @contact = user.build_contact(profile)
  26 + Contact.new params[:contact].merge(dest: profile)
21 27 end
22 28 end
23 29  
... ...
app/controllers/public/content_viewer_controller.rb
... ... @@ -126,7 +126,7 @@ class ContentViewerController &lt; ApplicationController
126 126 elsif !@page.display_to?(user)
127 127 if !profile.public?
128 128 private_profile_partial_parameters
129   - render :template => 'profile/_private_profile', :status => 403
  129 + render :template => 'profile/_private_profile', :status => 403, :formats => [:html]
130 130 allowed = false
131 131 else #if !profile.visible?
132 132 render_access_denied
... ... @@ -216,8 +216,6 @@ class ContentViewerController &lt; ApplicationController
216 216 if @page.has_posts?
217 217 posts = get_posts(params[:year], params[:month])
218 218  
219   - posts = posts.includes(:parent, {:profile => [:domains, :environment]}, :author)
220   -
221 219 #FIXME Need to run this before the pagination because this version of
222 220 # will_paginate returns a will_paginate collection instead of a
223 221 # relation.
... ...
app/controllers/public/home_controller.rb
... ... @@ -18,4 +18,10 @@ class HomeController &lt; PublicController
18 18 @no_design_blocks = true
19 19 end
20 20  
  21 + def welcome
  22 + @no_design_blocks = true
  23 + @display_confirmation_tips = !user.present? && !environment.enabled?(:skip_new_user_email_confirmation)
  24 + @person_template = user && user.template || params[:template_id] && Person.find(params[:template_id])
  25 + end
  26 +
21 27 end
... ...
app/controllers/public/invite_controller.rb
... ... @@ -4,8 +4,15 @@ class InviteController &lt; PublicController
4 4 before_filter :login_required
5 5 before_filter :check_permissions_to_invite
6 6  
7   - def select_address_book
  7 + def invite_friends
8 8 @import_from = params[:import_from] || "manual"
  9 + @mail_template = params[:mail_template] || environment.invitation_mail_template(profile)
  10 +
  11 + labels = Profile::SEARCHABLE_FIELDS.except(:nickname).merge(User::SEARCHABLE_FIELDS).map { |name,info| info[:label].downcase }
  12 + last = labels.pop
  13 + label = labels.join(', ')
  14 + @search_fields = "#{label} #{_('or')} #{last}"
  15 +
9 16 if request.post?
10 17 contact_list = ContactList.create
11 18 Delayed::Job.enqueue GetEmailContactsJob.new(@import_from, params[:login], params[:password], contact_list.id) if @import_from != 'manual'
... ... @@ -22,7 +29,7 @@ class InviteController &lt; PublicController
22 29 webmail_import_addresses = params[:webmail_import_addresses]
23 30 contacts_to_invite = Invitation.join_contacts(manual_import_addresses, webmail_import_addresses)
24 31 if !contacts_to_invite.empty?
25   - Delayed::Job.enqueue InvitationJob.new(current_user.person.id, contacts_to_invite, params[:mail_template], profile.id, @contact_list.id, locale)
  32 + Delayed::Job.enqueue InvitationJob.new(user.id, contacts_to_invite, params[:mail_template], profile.id, @contact_list.id, locale)
26 33 session[:notice] = _('Your invitations are being sent.')
27 34 if profile.person?
28 35 redirect_to :controller => 'profile', :action => 'friends'
... ... @@ -52,16 +59,36 @@ class InviteController &lt; PublicController
52 59 def cancel_fetching_emails
53 60 contact_list = ContactList.find(params[:contact_list])
54 61 contact_list.destroy
55   - redirect_to :action => 'select_address_book'
  62 + redirect_to :action => 'invite_friends'
  63 + end
  64 +
  65 + def invite_registered_friend
  66 + contacts_to_invite = params['q'].split(',')
  67 + if !contacts_to_invite.empty? && request.post?
  68 + Delayed::Job.enqueue InvitationJob.new(user.id, contacts_to_invite, '', profile.id, nil, locale)
  69 + session[:notice] = _('Your invitations are being sent.')
  70 + if profile.person?
  71 + redirect_to :controller => 'profile', :action => 'friends'
  72 + else
  73 + redirect_to :controller => 'profile', :action => 'members'
  74 + end
  75 + else
  76 + redirect_to :action => 'invite_friends'
  77 + session[:notice] = _('Please enter a valid profile.')
  78 + end
  79 + end
  80 +
  81 + def search
  82 + scope = profile.invite_friends_only ? user.friends : environment.people
  83 + scope = scope.not_members_of(profile) if profile.organization?
  84 + scope = scope.not_friends_of(profile) if profile.person?
  85 + results = find_by_contents(:people, environment, scope, params['q'], {:page => 1}, {:joins => :user})[:results]
  86 + render :text => prepare_to_token_input(results).to_json
56 87 end
57 88  
58 89 protected
59 90  
60 91 def check_permissions_to_invite
61   - if profile.person? and !current_user.person.has_permission?(:manage_friends, profile) or
62   - profile.community? and !current_user.person.has_permission?(:invite_members, profile)
63   - render_access_denied
64   - end
  92 + render_access_denied if !profile.allow_invitation_from?(user)
65 93 end
66   -
67 94 end
... ...
app/controllers/public/profile_controller.rb
... ... @@ -17,7 +17,11 @@ class ProfileController &lt; PublicController
17 17 end
18 18 @tags = profile.article_tags
19 19 unless profile.display_info_to?(user)
20   - profile.visible? ? private_profile : invisible_profile
  20 + if profile.visible?
  21 + private_profile
  22 + else
  23 + invisible_profile
  24 + end
21 25 end
22 26 end
23 27  
... ... @@ -61,13 +65,13 @@ class ProfileController &lt; PublicController
61 65  
62 66 def friends
63 67 if is_cache_expired?(profile.friends_cache_key(params))
64   - @friends = profile.friends.includes(relations_to_include).paginate(:per_page => per_page, :page => params[:npage])
  68 + @friends = profile.friends.includes(relations_to_include).paginate(:per_page => per_page, :page => params[:npage], :total_entries => profile.friends.count)
65 69 end
66 70 end
67 71  
68 72 def members
69 73 if is_cache_expired?(profile.members_cache_key(params))
70   - @members = profile.members_by_name.includes(relations_to_include).paginate(:per_page => members_per_page, :page => params[:npage])
  74 + @members = profile.members_by_name.includes(relations_to_include).paginate(:per_page => members_per_page, :page => params[:npage], :total_entries => profile.members.count)
71 75 end
72 76 end
73 77  
... ... @@ -315,7 +319,7 @@ class ProfileController &lt; PublicController
315 319 abuse_report = AbuseReport.new(params[:abuse_report])
316 320 if !params[:content_type].blank?
317 321 article = params[:content_type].constantize.find(params[:content_id])
318   - abuse_report.content = instance_eval(&article.reported_version)
  322 + abuse_report.content = article_reported_version(article)
319 323 end
320 324  
321 325 user.register_report(abuse_report, profile)
... ... @@ -394,6 +398,7 @@ class ProfileController &lt; PublicController
394 398  
395 399 def private_profile
396 400 private_profile_partial_parameters
  401 + render :action => 'index', :status => 403
397 402 end
398 403  
399 404 def invisible_profile
... ...
app/controllers/public/profile_search_controller.rb
... ... @@ -9,9 +9,12 @@ class ProfileSearchController &lt; PublicController
9 9 @q = params[:q]
10 10 unless @q.blank?
11 11 if params[:where] == 'environment'
12   - redirect_to :controller => 'search', :query => @q
  12 + # user is using global search, redirects to the search controller with
  13 + # the query
  14 + search_path = url_for(:controller => 'search', :query => @q)
  15 + request.xhr? ? render(:js => "window.location.href = #{search_path.to_json}") : redirect_to(search_path)
13 16 else
14   - @results = find_by_contents(:articles, profile.articles.published, @q, {:per_page => 10, :page => params[:page]})[:results]
  17 + @results = find_by_contents(:articles, profile, profile.articles.published, @q, {:per_page => 10, :page => params[:page]})[:results]
15 18 end
16 19 end
17 20 end
... ...
app/controllers/public/search_controller.rb
... ... @@ -4,11 +4,11 @@ class SearchController &lt; PublicController
4 4 include SearchHelper
5 5 include ActionView::Helpers::NumberHelper
6 6  
7   - before_filter :redirect_asset_param, :except => :assets
8   - before_filter :load_category
9   - before_filter :load_search_assets
10   - before_filter :load_query
11   - before_filter :load_filter
  7 + before_filter :redirect_asset_param, :except => [:assets, :suggestions]
  8 + before_filter :load_category, :except => :suggestions
  9 + before_filter :load_search_assets, :except => :suggestions
  10 + before_filter :load_query, :except => :suggestions
  11 + before_filter :load_order, :except => :suggestions
12 12  
13 13 # Backwards compatibility with old URLs
14 14 def redirect_asset_param
... ... @@ -20,7 +20,7 @@ class SearchController &lt; PublicController
20 20  
21 21 def index
22 22 @searches = {}
23   - @order = []
  23 + @assets = []
24 24 @names = {}
25 25 @results_only = true
26 26  
... ... @@ -28,7 +28,7 @@ class SearchController &lt; PublicController
28 28 load_query
29 29 @asset = key
30 30 send(key)
31   - @order << key
  31 + @assets << key
32 32 @names[key] = _(description)
33 33 end
34 34 @asset = nil
... ... @@ -42,7 +42,7 @@ class SearchController &lt; PublicController
42 42 # view the summary of one category
43 43 def category_index
44 44 @searches = {}
45   - @order = []
  45 + @assets = []
46 46 @names = {}
47 47 limit = MULTIPLE_SEARCH_LIMIT
48 48 [
... ... @@ -53,7 +53,7 @@ class SearchController &lt; PublicController
53 53 [ :communities, _('Communities'), :recent_communities ],
54 54 [ :articles, _('Contents'), :recent_articles ]
55 55 ].each do |asset, name, filter|
56   - @order << asset
  56 + @assets << asset
57 57 @searches[asset]= {:results => @category.send(filter, limit)}
58 58 raise "No total_entries for: #{asset}" unless @searches[asset][:results].respond_to?(:total_entries)
59 59 @names[asset] = name
... ... @@ -90,10 +90,14 @@ class SearchController &lt; PublicController
90 90 end
91 91  
92 92 def events
93   - year = (params[:year] ? params[:year].to_i : Date.today.year)
94   - month = (params[:month] ? params[:month].to_i : Date.today.month)
95   - day = (params[:day] ? params[:day].to_i : Date.today.day)
96   - @date = build_date(year, month, day)
  93 + if params[:year].blank? && params[:year].blank? && params[:day].blank?
  94 + @date = Date.today
  95 + else
  96 + year = (params[:year] ? params[:year].to_i : Date.today.year)
  97 + month = (params[:month] ? params[:month].to_i : Date.today.month)
  98 + day = (params[:day] ? params[:day].to_i : 1)
  99 + @date = build_date(year, month, day)
  100 + end
97 101 date_range = (@date - 1.month).at_beginning_of_month..(@date + 1.month).at_end_of_month
98 102  
99 103 @events = []
... ... @@ -143,12 +147,16 @@ class SearchController &lt; PublicController
143 147 render :partial => 'events/events'
144 148 end
145 149  
  150 + def suggestions
  151 + render :text => find_suggestions(normalize_term(params[:term]), environment, params[:asset]).to_json
  152 + end
  153 +
146 154 #######################################################
147 155 protected
148 156  
149 157 def load_query
150 158 @asset = (params[:asset] || params[:action]).to_sym
151   - @order ||= [@asset]
  159 + @assets ||= [@asset]
152 160 @searches ||= {}
153 161  
154 162 @query = params[:query] || ''
... ... @@ -169,13 +177,22 @@ class SearchController &lt; PublicController
169 177 end
170 178 end
171 179  
  180 + AVAILABLE_SEARCHES = ActiveSupport::OrderedHash[
  181 + :articles, _('Contents'),
  182 + :people, _('People'),
  183 + :communities, _('Communities'),
  184 + :enterprises, _('Enterprises'),
  185 + :products, _('Products and Services'),
  186 + :events, _('Events'),
  187 + ]
  188 +
172 189 def load_search_assets
173   - if SEARCHES.keys.include?(params[:action].to_sym) && environment.enabled?("disable_asset_#{params[:action]}")
  190 + if AVAILABLE_SEARCHES.keys.include?(params[:action].to_sym) && environment.enabled?("disable_asset_#{params[:action]}")
174 191 render_not_found
175 192 return
176 193 end
177 194  
178   - @enabled_searches = SEARCHES.select {|key, name| environment.disabled?("disable_asset_#{key}") }
  195 + @enabled_searches = AVAILABLE_SEARCHES.select {|key, name| environment.disabled?("disable_asset_#{key}") }
179 196 @searching = {}
180 197 @titles = {}
181 198 @enabled_searches.each do |key, name|
... ... @@ -185,11 +202,11 @@ class SearchController &lt; PublicController
185 202 @names = @titles if @names.nil?
186 203 end
187 204  
188   - def load_filter
189   - @filter = 'more_recent'
190   - if SEARCHES.keys.include?(@asset.to_sym)
191   - available_filters = asset_class(@asset)::SEARCH_FILTERS
192   - @filter = params[:filter] if available_filters.include?(params[:filter])
  205 + def load_order
  206 + @order = 'more_recent'
  207 + if AVAILABLE_SEARCHES.keys.include?(@asset.to_sym)
  208 + available_orders = asset_class(@asset)::SEARCH_FILTERS[:order]
  209 + @order = params[:order] if available_orders.include?(params[:order])
193 210 end
194 211 end
195 212  
... ... @@ -213,7 +230,7 @@ class SearchController &lt; PublicController
213 230 end
214 231  
215 232 def full_text_search
216   - @searches[@asset] = find_by_contents(@asset, @scope, @query, paginate_options, {:category => @category, :filter => @filter})
  233 + @searches[@asset] = find_by_contents(@asset, environment, @scope, @query, paginate_options, {:category => @category, :filter => @order})
217 234 end
218 235  
219 236 private
... ... @@ -228,4 +245,14 @@ class SearchController &lt; PublicController
228 245 20
229 246 end
230 247  
  248 + def available_assets
  249 + assets = ActiveSupport::OrderedHash[
  250 + :articles, _('Contents'),
  251 + :enterprises, _('Enterprises'),
  252 + :people, _('People'),
  253 + :communities, _('Communities'),
  254 + :products, _('Products and Services'),
  255 + ]
  256 + end
  257 +
231 258 end
... ...
app/helpers/application_helper.rb
... ... @@ -304,7 +304,7 @@ module ApplicationHelper
304 304 def partial_for_class(klass, prefix=nil, suffix=nil)
305 305 raise ArgumentError, 'No partial for object. Is there a partial for any class in the inheritance hierarchy?' if klass.nil?
306 306 name = klass.name.underscore
307   - controller.view_paths.reverse_each do |view_path|
  307 + controller.view_paths.each do |view_path|
308 308 partial = partial_for_class_in_view_path(klass, view_path, prefix, suffix)
309 309 return partial if partial
310 310 end
... ... @@ -433,19 +433,19 @@ module ApplicationHelper
433 433 end
434 434  
435 435 def theme_site_title
436   - theme_include('site_title')
  436 + @theme_site_title ||= theme_include 'site_title'
437 437 end
438 438  
439 439 def theme_header
440   - theme_include('header')
  440 + @theme_header ||= theme_include 'header'
441 441 end
442 442  
443 443 def theme_footer
444   - theme_include('footer')
  444 + @theme_footer ||= theme_include 'footer'
445 445 end
446 446  
447 447 def theme_extra_navigation
448   - theme_include('navigation')
  448 + @theme_extra_navigation ||= theme_include 'navigation'
449 449 end
450 450  
451 451 def is_testing_theme
... ... @@ -482,7 +482,12 @@ module ApplicationHelper
482 482 '/images/icons-app/enterprise-'+ size.to_s() +'.png'
483 483 end
484 484 else
485   - '/images/icons-app/person-'+ size.to_s() +'.png'
  485 + pixels = Image.attachment_options[:thumbnails][size].split('x').first
  486 + gravatar_profile_image_url(
  487 + profile.email,
  488 + :size => pixels,
  489 + :d => gravatar_default
  490 + )
486 491 end
487 492 filename = default_or_themed_icon(icon)
488 493 end
... ... @@ -602,7 +607,7 @@ module ApplicationHelper
602 607 end
603 608  
604 609 def gravatar_default
605   - (respond_to?(:theme_option) && theme_option.present? && theme_option['gravatar']) || NOOSFERO_CONF['gravatar']
  610 + (respond_to?(:theme_option) && theme_option.present? && theme_option['gravatar']) || NOOSFERO_CONF['gravatar'] || 'mm'
606 611 end
607 612  
608 613 attr_reader :environment
... ... @@ -669,13 +674,14 @@ module ApplicationHelper
669 674 html.join "\n"
670 675 end
671 676  
  677 + def theme_javascript_src
  678 + script = File.join theme_path, 'theme.js'
  679 + script if File.exists? File.join(Rails.root, 'public', script)
  680 + end
  681 +
672 682 def theme_javascript_ng
673   - script = File.join(theme_path, 'theme.js')
674   - if File.exists?(File.join(Rails.root, 'public', script))
675   - javascript_include_tag script
676   - else
677   - nil
678   - end
  683 + script = theme_javascript_src
  684 + javascript_include_tag script if script
679 685 end
680 686  
681 687 def file_field_or_thumbnail(label, image, i)
... ... @@ -856,8 +862,9 @@ module ApplicationHelper
856 862 end
857 863  
858 864 def base_url
859   - environment.top_url
  865 + environment.top_url(request.scheme)
860 866 end
  867 + alias :top_url :base_url
861 868  
862 869 def helper_for_article(article)
863 870 article_helper = ActionView::Base.new
... ... @@ -902,13 +909,15 @@ module ApplicationHelper
902 909 end
903 910  
904 911 def page_title
905   - (@page ? @page.title + ' - ' : '') +
906   - (@topic ? @topic.title + ' - ' : '') +
907   - (@section ? @section.title + ' - ' : '') +
908   - (@toc ? _('Online Manual') + ' - ' : '') +
909   - (controller.controller_name == 'chat' ? _('Chat') + ' - ' : '') +
910   - (profile ? profile.short_name : environment.name) +
911   - (@category ? " - #{@category.full_name}" : '')
  912 + CGI.escapeHTML(
  913 + (@page ? @page.title + ' - ' : '') +
  914 + (@topic ? @topic.title + ' - ' : '') +
  915 + (@section ? @section.title + ' - ' : '') +
  916 + (@toc ? _('Online Manual') + ' - ' : '') +
  917 + (controller.controller_name == 'chat' ? _('Chat') + ' - ' : '') +
  918 + (profile ? profile.short_name : environment.name) +
  919 + (@category ? " - #{@category.full_name}" : '')
  920 + )
912 921 end
913 922  
914 923 # DEPRECATED. Do not use this.
... ... @@ -937,9 +946,9 @@ module ApplicationHelper
937 946 # from Article model for an ArticleBlock.
938 947 def reference_to_article(text, article, anchor=nil)
939 948 if article.profile.domains.empty?
940   - href = "/#{article.url[:profile]}/"
  949 + href = "#{Noosfero.root}/#{article.url[:profile]}/"
941 950 else
942   - href = "http://#{article.profile.domains.first.name}/"
  951 + href = "http://#{article.profile.domains.first.name}#{Noosfero.root}/"
943 952 end
944 953 href += article.url[:page].join('/')
945 954 href += '#' + anchor if anchor
... ... @@ -1280,11 +1289,13 @@ module ApplicationHelper
1280 1289 end
1281 1290  
1282 1291 def delete_article_message(article)
1283   - if article.folder?
1284   - _("Are you sure that you want to remove the folder \"#{article.name}\"? Note that all the items inside it will also be removed!")
1285   - else
1286   - _("Are you sure that you want to remove the item \"#{article.name}\"?")
1287   - end
  1292 + CGI.escapeHTML(
  1293 + if article.folder?
  1294 + _("Are you sure that you want to remove the folder \"%s\"? Note that all the items inside it will also be removed!") % article.name
  1295 + else
  1296 + _("Are you sure that you want to remove the item \"%s\"?") % article.name
  1297 + end
  1298 + )
1288 1299 end
1289 1300  
1290 1301 def expirable_link_to(expired, content, url, options = {})
... ... @@ -1296,8 +1307,19 @@ module ApplicationHelper
1296 1307 end
1297 1308 end
1298 1309  
1299   - def remove_content_button(action)
1300   - @plugins.dispatch("content_remove_#{action.to_s}", @page).include?(true)
  1310 + def content_remove_spread(content)
  1311 + !content.public? || content.folder? || (profile == user && user.communities.blank? && !environment.portal_enabled)
  1312 + end
  1313 +
  1314 + def remove_content_button(action, content)
  1315 + method_name = "content_remove_#{action.to_s}"
  1316 + plugin_condition = @plugins.dispatch(method_name, content).include?(true)
  1317 + begin
  1318 + core_condition = self.send(method_name, content)
  1319 + rescue NoMethodError
  1320 + core_condition = false
  1321 + end
  1322 + core_condition || plugin_condition
1301 1323 end
1302 1324  
1303 1325 def template_options(kind, field_name)
... ... @@ -1305,10 +1327,8 @@ module ApplicationHelper
1305 1327 return '' if templates.count == 0
1306 1328 return hidden_field_tag("#{field_name}[template_id]", templates.first.id) if templates.count == 1
1307 1329  
1308   - counter = 0
1309 1330 radios = templates.map do |template|
1310   - counter += 1
1311   - content_tag('li', labelled_radio_button(link_to(template.name, template.url, :target => '_blank'), "#{field_name}[template_id]", template.id, counter==1))
  1331 + content_tag('li', labelled_radio_button(link_to(template.name, template.url, :target => '_blank'), "#{field_name}[template_id]", template.id, environment.is_default_template?(template)))
1312 1332 end.join("\n")
1313 1333  
1314 1334 content_tag('div', content_tag('label', _('Profile organization'), :for => 'template-options', :class => 'formlabel') +
... ... @@ -1372,7 +1392,7 @@ module ApplicationHelper
1372 1392 # are old things that do not support it we are keeping this hot spot.
1373 1393 html = @plugins.pipeline(:parse_content, html, source).first
1374 1394 end
1375   - html
  1395 + html && html.html_safe
1376 1396 end
1377 1397  
1378 1398 def convert_macro(html, source)
... ... @@ -1402,4 +1422,51 @@ module ApplicationHelper
1402 1422 content_tag('ul', article.versions.map {|v| link_to("r#{v.version}", @page.url.merge(:version => v.version))})
1403 1423 end
1404 1424  
  1425 + def search_input_with_suggestions(name, asset, default, options = {})
  1426 + text_field_tag name, default, options.merge({:class => 'search-input-with-suggestions', 'data-asset' => asset})
  1427 + end
  1428 +
  1429 + def profile_suggestion_profile_connections(suggestion)
  1430 + profiles = suggestion.profile_connections.first(4).map do |profile|
  1431 + link_to(profile_image(profile, :icon, :title => profile.name), profile.url, :class => 'profile-suggestion-connection-icon')
  1432 + end
  1433 +
  1434 + controller_target = suggestion.suggestion_type == 'Person' ? :friends : :memberships
  1435 + profiles << link_to("<big> +#{suggestion.profile_connections.count - 4}</big>", :controller => controller_target, :action => :connections, :id => suggestion.suggestion_id) if suggestion.profile_connections.count > 4
  1436 +
  1437 + if profiles.present?
  1438 + content_tag(:div, profiles.join , :class => 'profile-connections')
  1439 + else
  1440 + ''
  1441 + end
  1442 + end
  1443 +
  1444 + def profile_suggestion_tag_connections(suggestion)
  1445 + tags = suggestion.tag_connections.first(4).map do |tag|
  1446 + tag.name + ', '
  1447 + end
  1448 + last_tag = tags.pop
  1449 + tags << last_tag.strip.chop if last_tag.present?
  1450 + title = tags.join
  1451 +
  1452 + controller_target = suggestion.suggestion_type == 'Person' ? :friends : :memberships
  1453 + tags << ' ' + link_to('...', {:controller => controller_target, :action => :connections, :id => suggestion.suggestion_id}, :class => 'more-tag-connections', :title => _('See all connections')) if suggestion.tag_connections.count > 4
  1454 +
  1455 + if tags.present?
  1456 + content_tag(:div, tags.join, :class => 'tag-connections', :title => title)
  1457 + else
  1458 + ''
  1459 + end
  1460 + end
  1461 +
  1462 + def labelled_colorpicker_field(human_name, object_name, method, options = {})
  1463 + options[:id] ||= 'text-field-' + FormsHelper.next_id_number
  1464 + content_tag('label', human_name, :for => options[:id], :class => 'formlabel') +
  1465 + colorpicker_field(object_name, method, options.merge(:class => 'colorpicker_field'))
  1466 + end
  1467 +
  1468 + def colorpicker_field(object_name, method, options = {})
  1469 + text_field(object_name, method, options.merge(:class => 'colorpicker_field'))
  1470 + end
  1471 +
1405 1472 end
... ...
app/helpers/article_helper.rb
... ... @@ -3,6 +3,12 @@ module ArticleHelper
3 3 include PrototypeHelper
4 4 include TokenHelper
5 5  
  6 + def article_reported_version(article)
  7 + search_path = Rails.root.join('app', 'views', 'shared', 'reported_versions')
  8 + partial_path = File.join('shared', 'reported_versions', 'profile', partial_for_class_in_view_path(article.class, search_path))
  9 + render_to_string(:partial => partial_path, :locals => {:article => article})
  10 + end
  11 +
6 12 def custom_options_for_article(article, tokenized_children)
7 13 @article = article
8 14  
... ... @@ -71,18 +77,69 @@ module ArticleHelper
71 77 content_tag('div',
72 78 radio_button(:article, :published, false) +
73 79 content_tag('label', _('Private'), :for => 'article_published_false', :id => "label_private")
74   - ) +
75   - (article.profile.community? ? content_tag('div',
76   - content_tag('label', _('Fill in the search field to add the exception users to see this content'), :id => "text-input-search-exception-users") +
77   - token_input_field_tag(:q, 'search-article-privacy-exceptions', {:action => 'search_article_privacy_exceptions'},
78   - {:focus => false, :hint_text => _('Type in a search term for a user'), :pre_populate => tokenized_children})) :
79   - ''))
  80 + ) +
  81 + privacity_exceptions(article, tokenized_children)
  82 + )
  83 + end
  84 +
  85 + def privacity_exceptions(article, tokenized_children)
  86 + content_tag('div',
  87 + content_tag('div',
  88 + (
  89 + if article.profile
  90 + add_option_to_followers(article, tokenized_children)
  91 + else
  92 + ''
  93 + end
  94 + )
  95 + ),
  96 + :style => "margin-left:10px"
  97 + )
  98 + end
  99 +
  100 + def add_option_to_followers(article, tokenized_children)
  101 + label_message = article.profile.organization? ? _('For all community members') : _('For all your friends')
  102 +
  103 + check_box(
  104 + :article,
  105 + :show_to_followers,
  106 + {:class => "custom_privacy_option"}
  107 + ) +
  108 + content_tag(
  109 + 'label',
  110 + label_message,
  111 + :for => 'article_show_to_followers',
  112 + :id => 'label_show_to_followers'
  113 + ) +
  114 + (article.profile.community? ?
  115 + content_tag(
  116 + 'div',
  117 + content_tag(
  118 + 'label',
  119 + _('Fill in the search field to add the exception users to see this content'),
  120 + :id => "text-input-search-exception-users"
  121 + ) +
  122 + token_input_field_tag(
  123 + :q,
  124 + 'search-article-privacy-exceptions',
  125 + {:action => 'search_article_privacy_exceptions'},
  126 + {
  127 + :focus => false,
  128 + :hint_text => _('Type in a search term for a user'),
  129 + :pre_populate => tokenized_children
  130 + }
  131 + )
  132 + ) : '')
80 133 end
81 134  
82 135 def prepare_to_token_input(array)
83 136 array.map { |object| {:id => object.id, :name => object.name} }
84 137 end
85 138  
  139 + def prepare_to_token_input_by_label(array)
  140 + array.map { |object| {:label => object.name, :value => object.name} }
  141 + end
  142 +
86 143 def cms_label_for_new_children
87 144 _('New article')
88 145 end
... ...
app/helpers/boxes_helper.rb
... ... @@ -170,49 +170,54 @@ module BoxesHelper
170 170 else
171 171 "before-block-#{block.id}"
172 172 end
173   -
174   - content_tag('div', '&nbsp;', :id => id, :class => 'block-target' ) + drop_receiving_element(id, :url => { :action => 'move_block', :target => id }, :accept => box.acceptable_blocks, :hoverclass => 'block-target-hover')
  173 + if block.nil? or modifiable?(block)
  174 + content_tag('div', '&nbsp;', :id => id, :class => 'block-target' ) + drop_receiving_element(id, :url => { :action => 'move_block', :target => id }, :accept => box.acceptable_blocks, :hoverclass => 'block-target-hover')
  175 + else
  176 + ""
  177 + end
175 178 end
176 179  
177 180 # makes the given block draggable so it can be moved away.
178 181 def block_handle(block)
179   - draggable_element("block-#{block.id}", :revert => true)
  182 + modifiable?(block) ? draggable_element("block-#{block.id}", :revert => true) : ""
180 183 end
181 184  
182 185 def block_edit_buttons(block)
183 186 buttons = []
184 187 nowhere = 'javascript: return false;'
185 188  
186   - if block.first?
187   - buttons << icon_button('up-disabled', _("Can't move up anymore."), nowhere)
188   - else
189   - buttons << icon_button('up', _('Move block up'), { :action => 'move_block_up', :id => block.id }, { :method => 'post' })
190   - end
  189 + if modifiable?(block)
  190 + if block.first?
  191 + buttons << icon_button('up-disabled', _("Can't move up anymore."), nowhere)
  192 + else
  193 + buttons << icon_button('up', _('Move block up'), { :action => 'move_block_up', :id => block.id }, { :method => 'post' })
  194 + end
191 195  
192   - if block.last?
193   - buttons << icon_button('down-disabled', _("Can't move down anymore."), nowhere)
194   - else
195   - buttons << icon_button(:down, _('Move block down'), { :action => 'move_block_down' ,:id => block.id }, { :method => 'post'})
196   - end
  196 + if block.last?
  197 + buttons << icon_button('down-disabled', _("Can't move down anymore."), nowhere)
  198 + else
  199 + buttons << icon_button(:down, _('Move block down'), { :action => 'move_block_down' ,:id => block.id }, { :method => 'post'})
  200 + end
197 201  
198   - holder = block.owner
199   - # move to opposite side
200   - # FIXME too much hardcoded stuff
201   - if holder.layout_template == 'default'
202   - if block.box.position == 2 # area 2, left side => move to right side
203   - buttons << icon_button('right', _('Move to the opposite side'), { :action => 'move_block', :target => 'end-of-box-' + holder.boxes[2].id.to_s, :id => block.id }, :method => 'post' )
204   - elsif block.box.position == 3 # area 3, right side => move to left side
205   - buttons << icon_button('left', _('Move to the opposite side'), { :action => 'move_block', :target => 'end-of-box-' + holder.boxes[1].id.to_s, :id => block.id }, :method => 'post' )
  202 + holder = block.owner
  203 + # move to opposite side
  204 + # FIXME too much hardcoded stuff
  205 + if holder.layout_template == 'default'
  206 + if block.box.position == 2 # area 2, left side => move to right side
  207 + buttons << icon_button('right', _('Move to the opposite side'), { :action => 'move_block', :target => 'end-of-box-' + holder.boxes[2].id.to_s, :id => block.id }, :method => 'post' )
  208 + elsif block.box.position == 3 # area 3, right side => move to left side
  209 + buttons << icon_button('left', _('Move to the opposite side'), { :action => 'move_block', :target => 'end-of-box-' + holder.boxes[1].id.to_s, :id => block.id }, :method => 'post' )
  210 + end
206 211 end
207   - end
208 212  
209   - if block.editable?
210   - buttons << colorbox_icon_button(:edit, _('Edit'), { :action => 'edit', :id => block.id })
211   - end
  213 + if block.editable?
  214 + buttons << colorbox_icon_button(:edit, _('Edit'), { :action => 'edit', :id => block.id })
  215 + end
212 216  
213   - if !block.main?
214   - buttons << icon_button(:delete, _('Remove block'), { :action => 'remove', :id => block.id }, { :method => 'post', :confirm => _('Are you sure you want to remove this block?')})
215   - buttons << icon_button(:clone, _('Clone'), { :action => 'clone_block', :id => block.id }, { :method => 'post' })
  217 + if !block.main?
  218 + buttons << icon_button(:delete, _('Remove block'), { :action => 'remove', :id => block.id }, { :method => 'post', :confirm => _('Are you sure you want to remove this block?')})
  219 + buttons << icon_button(:clone, _('Clone'), { :action => 'clone_block', :id => block.id }, { :method => 'post' })
  220 + end
216 221 end
217 222  
218 223 if block.respond_to?(:help)
... ... @@ -248,5 +253,7 @@ module BoxesHelper
248 253 classes
249 254 end
250 255  
251   -
  256 + def modifiable?(block)
  257 + return !block.fixed || environment.admins.include?(user)
  258 + end
252 259 end
... ...
app/helpers/categories_helper.rb
1 1 module CategoriesHelper
2 2  
3   -
4   - COLORS = [
5   - [ N_('Do not display at the menu'), nil ],
6   - [ N_('Orange'), 1],
7   - [ N_('Green'), 2],
8   - [ N_('Purple'), 3],
9   - [ N_('Red'), 4],
10   - [ N_('Dark Green'), 5],
11   - [ N_('Blue Oil'), 6],
12   - [ N_('Blue'), 7],
13   - [ N_('Brown'), 8],
14   - [ N_('Light Green'), 9],
15   - [ N_('Light Blue'), 10],
16   - [ N_('Dark Blue'), 11],
17   - [ N_('Blue Pool'), 12],
18   - [ N_('Beige'), 13],
19   - [ N_('Yellow'), 14],
20   - [ N_('Light Brown'), 15]
21   - ]
22   -
23 3 TYPES = [
24 4 [ _('General Category'), Category.to_s ],
25 5 [ _('Product Category'), ProductCategory.to_s ],
26 6 [ _('Region'), Region.to_s ],
27 7 ]
28 8  
29   - def select_color_for_category
30   - if @category.top_level?
31   - labelled_form_field(_('Display at the menu?'), select('category', 'display_color', CategoriesHelper::COLORS.map {|item| [gettext(item[0]), item[1]] }))
32   - else
33   - ""
34   - end
35   - end
36   -
37   - def display_color_for_category(category)
38   - color = category.display_color
39   - if color.nil?
40   - ""
41   - else
42   - "[" + gettext(CategoriesHelper::COLORS.find {|item| item[1] == color}.first) + "]"
43   - end
44   - end
45   -
46 9 def select_category_type(field)
47 10 value = params[field]
48 11 labelled_form_field(_('Type of category'), select_tag('type', options_for_select(TYPES, value)))
49 12 end
50 13  
  14 + def category_color_style(category)
  15 + return '' if category.nil? or category.display_color.blank?
  16 + 'background-color: #'+category.display_color+';'
  17 + end
  18 +
51 19 #FIXME make this test
52 20 def selected_category_link(cat)
53 21 js_remove = "jQuery('#selected-category-#{cat.id}').remove();"
... ...
app/helpers/cms_helper.rb
... ... @@ -40,12 +40,8 @@ module CmsHelper
40 40 end
41 41 end
42 42  
43   - def display_spread_button(profile, article)
44   - if profile.person?
45   - expirable_button article, :spread, _('Spread this'), :action => 'publish', :id => article.id
46   - elsif profile.community? && environment.portal_community
47   - expirable_button article, :spread, _('Spread this'), :action => 'publish_on_portal_community', :id => article.id
48   - end
  43 + def display_spread_button(article)
  44 + expirable_button article, :spread, _('Spread this'), {:action => 'publish', :id => article.id}, {:class => 'colorbox'}
49 45 end
50 46  
51 47 def display_delete_button(article)
... ...
app/helpers/content_viewer_helper.rb
... ... @@ -10,7 +10,7 @@ module ContentViewerHelper
10 10 end
11 11  
12 12 def number_of_comments(article)
13   - display_number_of_comments(article.comments_count - article.spam_comments_count)
  13 + display_number_of_comments(article.comments_count - article.spam_comments_count.to_i)
14 14 end
15 15  
16 16 def article_title(article, args = {})
... ... @@ -45,7 +45,7 @@ module ContentViewerHelper
45 45 { article.environment.locales[translation.language] => { :href => url_for(translation.url) } }
46 46 end
47 47 content_tag(:div, link_to(_('Translations'), '#',
48   - :onmouseover => "toggleSubmenu(this, '#{_('Translations')}', #{links.to_json}); return false",
  48 + :onmouseover => "toggleSubmenu(this, '#{_('Translations')}', #{CGI::escape_html(links.to_json)}); return false",
49 49 :class => 'article-translations-menu simplemenu-trigger up'),
50 50 :class => 'article-translations')
51 51 end
... ...
app/helpers/forms_helper.rb
... ... @@ -265,7 +265,7 @@ module FormsHelper
265 265 )
266 266 end
267 267  
268   - def select_profile_folder(label_text, field_id, profile, default_value='', html_options = {}, js_options = {}, find_options = {})
  268 + def select_profile_folder(label_text, field_id, profile, default_value='', html_options = {}, js_options = {}, find_options = {}, extra_options = {})
269 269 if find_options.empty?
270 270 folders = profile.folders
271 271 else
... ... @@ -276,7 +276,7 @@ module FormsHelper
276 276 select_tag(
277 277 field_id,
278 278 options_for_select(
279   - [[profile.identifier, '']] +
  279 + [[(extra_options[:root_label] || profile.identifier), '']] +
280 280 folders.collect {|f| [ profile.identifier + '/' + f.full_name, f.id.to_s ] },
281 281 default_value.to_s
282 282 ),
... ...
app/helpers/layout_helper.rb
... ... @@ -2,12 +2,31 @@ module LayoutHelper
2 2  
3 3 def body_classes
4 4 # Identify the current controller and action for the CSS:
  5 + (logged_in? ? " logged-in" : "") +
5 6 " controller-#{controller.controller_name}" +
6 7 " action-#{controller.controller_name}-#{controller.action_name}" +
7 8 " template-#{@layout_template || if profile.blank? then 'default' else profile.layout_template end}" +
8 9 (!profile.nil? && profile.is_on_homepage?(request.path,@page) ? " profile-homepage" : "")
9 10 end
10 11  
  12 + def html_tag_classes
  13 + [
  14 + body_classes, (
  15 + profile.blank? ? nil : [
  16 + 'profile-type-is-' + profile.class.name.downcase,
  17 + 'profile-name-is-' + profile.identifier,
  18 + ]
  19 + ), 'theme-' + current_theme,
  20 + @plugins.dispatch(:html_tag_classes).map do |content|
  21 + if content.respond_to?(:call)
  22 + instance_exec(&content)
  23 + else
  24 + content.html_safe
  25 + end
  26 + end
  27 + ].flatten.compact.join(' ')
  28 + end
  29 +
11 30 def noosfero_javascript
12 31 plugins_javascripts = @plugins.map { |plugin| [plugin.js_files].flatten.map { |js| plugin.class.public_path(js) } }.flatten
13 32  
... ... @@ -17,6 +36,8 @@ module LayoutHelper
17 36 unless plugins_javascripts.empty?
18 37 output += javascript_include_tag plugins_javascripts, :cache => "cache/plugins-#{Digest::MD5.hexdigest plugins_javascripts.to_s}"
19 38 end
  39 + output += theme_javascript_ng.to_s
  40 +
20 41 output
21 42 end
22 43  
... ... @@ -27,6 +48,8 @@ module LayoutHelper
27 48 'thickbox',
28 49 'lightbox',
29 50 'colorbox',
  51 + 'selectordie',
  52 + 'inputosaurus',
30 53 pngfix_stylesheet_path,
31 54 ] + tokeninput_stylesheets
32 55 plugins_stylesheets = @plugins.select(&:stylesheet?).map { |plugin| plugin.class.public_path('style.css') }
... ... @@ -83,6 +106,10 @@ module LayoutHelper
83 106 theme_path + '/style.css'
84 107 end
85 108  
  109 + def layout_template
  110 + if profile then profile.layout_template else environment.layout_template end
  111 + end
  112 +
86 113 def addthis_javascript
87 114 if NOOSFERO_CONF['addthis_enabled']
88 115 '<script src="https://s7.addthis.com/js/152/addthis_widget.js"></script>'
... ... @@ -90,7 +117,7 @@ module LayoutHelper
90 117 end
91 118  
92 119 def meta_description_tag(article=nil)
93   - article ? truncate(strip_tags(article.body.to_s), :length => 200) : environment.name
  120 + article ? CGI.escapeHTML(truncate(strip_tags(article.body.to_s), :length => 200)) : environment.name
94 121 end
95 122 end
96 123  
... ...
app/helpers/person_notifier_helper.rb
... ... @@ -1,15 +0,0 @@
1   -module PersonNotifierHelper
2   -
3   - include ApplicationHelper
4   -
5   - private
6   -
7   - def path_to_image(source)
8   - top_url + source
9   - end
10   -
11   - def top_url
12   - top_url = @profile.environment ? @profile.environment.top_url : ''
13   - end
14   -
15   -end
app/helpers/profile_helper.rb
1 1 module ProfileHelper
2 2  
3   - def display_field(title, profile, field, force = false)
  3 + COMMON_CATEGORIES = ActiveSupport::OrderedHash.new
  4 + COMMON_CATEGORIES[:content] = [:blogs, :image_galleries, :events, :article_tags]
  5 + COMMON_CATEGORIES[:interests] = [:interests]
  6 + COMMON_CATEGORIES[:general] = nil
  7 +
  8 + PERSON_CATEGORIES = ActiveSupport::OrderedHash.new
  9 + PERSON_CATEGORIES[:basic_information] = [:nickname, :sex, :birth_date, :location, :privacy_setting, :created_at]
  10 + PERSON_CATEGORIES[:contact] = [:contact_phone, :cell_phone, :comercial_phone, :contact_information, :email, :personal_website, :jabber_id]
  11 + PERSON_CATEGORIES[:location] = [:address, :address_reference, :zip_code, :city, :state, :district, :country, :nationality]
  12 + PERSON_CATEGORIES[:work] = [:organization, :organization_website, :professional_activity]
  13 + PERSON_CATEGORIES[:study] = [:schooling, :formation, :area_of_study]
  14 + PERSON_CATEGORIES[:network] = [:friends, :communities, :enterprises]
  15 + PERSON_CATEGORIES.merge!(COMMON_CATEGORIES)
  16 +
  17 + ORGANIZATION_CATEGORIES = ActiveSupport::OrderedHash.new
  18 + ORGANIZATION_CATEGORIES[:basic_information] = [:display_name, :created_at, :foundation_year, :type, :language, :members_count, :location, :address_reference, :historic_and_current_context, :admins]
  19 + ORGANIZATION_CATEGORIES[:contact] = [:contact_person, :contact_phone, :contact_email, :organization_website, :jabber_id]
  20 + ORGANIZATION_CATEGORIES[:economic] = [:business_name, :acronym, :economic_activity, :legal_form, :products, :activities_short_description, :management_information]
  21 + ORGANIZATION_CATEGORIES.merge!(COMMON_CATEGORIES)
  22 +
  23 + CATEGORY_MAP = ActiveSupport::OrderedHash.new
  24 + CATEGORY_MAP[:person] = PERSON_CATEGORIES
  25 + CATEGORY_MAP[:organization] = ORGANIZATION_CATEGORIES
  26 +
  27 + FORCE = {
  28 + :person => [:privacy_setting],
  29 + :organization => [:privacy_setting, :location],
  30 + }
  31 +
  32 + MULTIPLE = {
  33 + :person => [:blogs, :image_galleries, :interests],
  34 + :organization => [:blogs, :image_galleries, :interests],
  35 + }
  36 +
  37 + CUSTOM_LABELS = {
  38 + :zip_code => _('ZIP code'),
  39 + :email => _('e-Mail'),
  40 + :jabber_id => _('Jabber'),
  41 + :birth_date => _('Date of birth'),
  42 + :created_at => _('Profile created at'),
  43 + :members_count => _('Members'),
  44 + :privacy_setting => _('Privacy setting'),
  45 + :article_tags => _('Tags')
  46 + }
  47 +
  48 + EXCEPTION = {
  49 + :person => [:image, :preferred_domain, :description, :tag_list],
  50 + :organization => [:image, :preferred_domain, :description, :tag_list, :address, :zip_code, :city, :state, :country, :district]
  51 + }
  52 +
  53 + def general_fields
  54 + categorized_fields = CATEGORY_MAP[kind].values.flatten
  55 + profile.class.fields.map(&:to_sym) - categorized_fields - EXCEPTION[kind]
  56 + end
  57 +
  58 + def kind
  59 + if profile.kind_of?(Person)
  60 + :person
  61 + else
  62 + :organization
  63 + end
  64 + end
  65 +
  66 + def title(field, entry = nil)
  67 + return self.send("#{field}_custom_title", entry) if MULTIPLE[kind].include?(field) && entry.present?
  68 + CUSTOM_LABELS[field.to_sym] || _(field.to_s.humanize)
  69 + end
  70 +
  71 + def display_field(field)
  72 + force = FORCE[kind].include?(field)
  73 + multiple = MULTIPLE[kind].include?(field)
4 74 unless force || profile.may_display_field_to?(field, user)
5 75 return ''
6 76 end
7   - value = profile.send(field)
8   - if !value.blank?
9   - if block_given?
10   - value = yield(value)
11   - end
12   - content_tag('tr', content_tag('td', title, :class => 'field-name') + content_tag('td', value))
  77 + value = begin profile.send(field) rescue nil end
  78 + return '' if value.blank?
  79 + if value.kind_of?(Hash)
  80 + content = self.send("treat_#{field}", value)
  81 + content_tag('tr', content_tag('td', title(field), :class => 'field-name') + content_tag('td', content))
13 82 else
14   - ''
  83 + entries = multiple ? value : [] << value
  84 + entries.map do |entry|
  85 + content = self.send("treat_#{field}", entry)
  86 + unless content.blank?
  87 + content_tag('tr', content_tag('td', title(field, entry), :class => 'field-name') + content_tag('td', content))
  88 + end
  89 + end.join("\n")
15 90 end
16 91 end
17 92  
18   - def display_contact(profile)
19   - fields = []
20   - fields << display_field(_('Address:'), profile, :address).html_safe
21   - fields << display_field(_('ZIP code:'), profile, :zip_code).html_safe
22   - fields << display_field(_('Contact phone:'), profile, :contact_phone).html_safe
23   - fields << display_field(_('e-Mail:'), profile, :email) { |email| link_to_email(email) }.html_safe
24   - fields << display_field(_('Personal website:'), profile, :personal_website).html_safe
25   - fields << display_field(_('Jabber:'), profile, :jabber_id).html_safe
26   - if fields.reject!(&:blank?).empty?
27   - ''
28   - else
29   - content_tag('tr', content_tag('th', _('Contact'), { :colspan => 2 })) + fields.join.html_safe
  93 + def treat_email(email)
  94 + link_to_email(email)
  95 + end
  96 +
  97 + def treat_organization_website(url)
  98 + link_to(url, url)
  99 + end
  100 +
  101 + def treat_sex(gender)
  102 + { 'male' => _('Male'), 'female' => _('Female') }[gender]
  103 + end
  104 +
  105 + def treat_date(date)
  106 + show_date(date.to_date)
  107 + end
  108 + alias :treat_birth_date :treat_date
  109 + alias :treat_created_at :treat_date
  110 +
  111 + def treat_friends(friends)
  112 + link_to friends.count, :controller => 'profile', :action => 'friends'
  113 + end
  114 +
  115 + def treat_communities(communities)
  116 + link_to communities.count, :controller => "profile", :action => 'communities'
  117 + end
  118 +
  119 + def treat_enterprises(enterprises)
  120 + if environment.disabled?('disable_asset_enterprises')
  121 + link_to enterprises.count, :controller => "profile", :action => 'enterprises'
  122 + end
  123 + end
  124 +
  125 + def treat_members_count(count)
  126 + link_to count, :controller => 'profile', :action => 'members'
  127 + end
  128 +
  129 + def treat_products(products)
  130 + if profile.kind_of?(Enterprise) && profile.environment.enabled?('products_for_enterprises')
  131 + link_to _('Products/Services'), :controller => 'catalog', :action => 'index'
30 132 end
31 133 end
32 134  
33   - def display_work_info(profile)
34   - organization = display_field(_('Organization:'), profile, :organization)
35   - organization_site = display_field(_('Organization website:'), profile, :organization_website) { |url| link_to(url, url) }
36   - if organization.blank? && organization_site.blank?
37   - ''
  135 + def treat_admins(admins)
  136 + profile.admins.map { |admin| link_to(admin.short_name, admin.url)}.join(', ')
  137 + end
  138 +
  139 + def treat_blogs(blog)
  140 + link_to(n_('One post', '%{num} posts', blog.posts.published.count) % { :num => blog.posts.published.count }, blog.url)
  141 + end
  142 +
  143 + def treat_image_galleries(gallery)
  144 + link_to(n_('One picture', '%{num} pictures', gallery.images.published.count) % { :num => gallery.images.published.count }, gallery.url)
  145 + end
  146 +
  147 + def treat_events(events)
  148 + link_to events.published.count, :controller => 'events', :action => 'events'
  149 + end
  150 +
  151 + def treat_article_tags(tags)
  152 + tag_cloud @tags, :id, { :action => 'tags' }, :max_size => 18, :min_size => 10
  153 + end
  154 +
  155 + def treat_interests(interest)
  156 + link_to interest.name, :controller => 'search', :action => 'category_index', :category_path => interest.explode_path
  157 + end
  158 +
  159 + def article_custom_title(article)
  160 + article.name
  161 + end
  162 + alias :blogs_custom_title :article_custom_title
  163 + alias :image_galleries_custom_title :article_custom_title
  164 +
  165 + def interests_custom_title(interest)
  166 + ''
  167 + end
  168 +
  169 + def method_missing(method, *args, &block)
  170 + if method.to_s =~ /^treat_(.+)$/
  171 + args[0]
  172 + elsif method.to_s =~ /^display_(.+)$/ && CATEGORY_MAP[kind].has_key?($1.to_sym)
  173 + category = $1.to_sym
  174 + fields = category == :general ? general_fields : CATEGORY_MAP[kind][category]
  175 + contents = []
  176 +
  177 + fields.each do |field|
  178 + contents << display_field(field).html_safe
  179 + end
  180 +
  181 + contents = contents.delete_if(&:blank?)
  182 +
  183 + unless contents.empty?
  184 + content_tag('tr', content_tag('th', title(category), { :colspan => 2 })) + contents.join.html_safe
  185 + else
  186 + ''
  187 + end
38 188 else
39   - content_tag('tr', content_tag('th', _('Work'), { :colspan => 2 })) + organization + organization_site
  189 + super
40 190 end
41 191 end
42 192  
... ...
app/helpers/role_helper.rb
1 1 module RoleHelper
  2 +
  3 + def role_available_permissions(role)
  4 + role.kind == "Environment" ? ['Environment', 'Profile'] : [role.kind]
  5 + end
  6 +
2 7 end
... ...
app/helpers/search_helper.rb
... ... @@ -5,22 +5,31 @@ module SearchHelper
5 5 BLOCKS_SEARCH_LIMIT = 24
6 6 MULTIPLE_SEARCH_LIMIT = 8
7 7  
8   - SEARCHES = ActiveSupport::OrderedHash[
9   - :articles, _('Contents'),
10   - :enterprises, _('Enterprises'),
11   - :people, _('People'),
12   - :communities, _('Communities'),
13   - :products, _('Products and Services'),
14   - :events, _('Events'),
15   - ]
  8 + FILTERS_TRANSLATIONS = {
  9 + :order => _('Order'),
  10 + :display => _('Display')
  11 + }
16 12  
17   - FILTER_TRANSLATION = {
18   - 'more_popular' => _('More popular'),
19   - 'more_active' => _('More active'),
20   - 'more_recent' => _('More recent'),
21   - 'more_comments' => _('More comments')
  13 + FILTERS_OPTIONS_TRANSLATION = {
  14 + :order => {
  15 + 'more_popular' => _('More popular'),
  16 + 'more_active' => _('More active'),
  17 + 'more_recent' => _('More recent'),
  18 + 'more_comments' => _('More comments')
  19 + },
  20 + :display => {
  21 + 'map' => _('Map'),
  22 + 'full' => _('Full'),
  23 + 'compact' => _('Compact')
  24 + }
22 25 }
23 26  
  27 + COMMON_PROFILE_LIST_BLOCK = [
  28 + :enterprises,
  29 + :people,
  30 + :communities
  31 + ]
  32 +
24 33 # FIXME remove it after search_controler refactored
25 34 include EventsHelper
26 35  
... ... @@ -50,7 +59,7 @@ module SearchHelper
50 59 end
51 60  
52 61 def display?(asset, mode)
53   - defined?(asset_class(asset)::SEARCH_DISPLAYS) && asset_class(asset)::SEARCH_DISPLAYS.include?(mode.to_s)
  62 + defined?(asset_class(asset)::SEARCH_FILTERS[:display]) && asset_class(asset)::SEARCH_FILTERS[:display].include?(mode.to_s)
54 63 end
55 64  
56 65 def display_results(searches=nil, asset=nil)
... ... @@ -87,6 +96,16 @@ module SearchHelper
87 96 end
88 97 end
89 98  
  99 + def select_filter(name, options, default = nil)
  100 + if options.size <= 1
  101 + return
  102 + else
  103 + options = options.map {|option| [FILTERS_OPTIONS_TRANSLATION[name][option], option]}
  104 + options = options_for_select(options, :selected => (params[name] || default))
  105 + select_tag(name, options)
  106 + end
  107 + end
  108 +
90 109 def display_selector(asset, display, float = 'right')
91 110 display = nil if display.blank?
92 111 display ||= asset_class(asset).default_search_display
... ... @@ -94,43 +113,39 @@ module SearchHelper
94 113 compact_link = display?(asset, :compact) ? (display == 'compact' ? _('Compact') : link_to(_('Compact'), params.merge(:display => 'compact'))) : nil
95 114 map_link = display?(asset, :map) ? (display == 'map' ? _('Map') : link_to(_('Map'), params.merge(:display => 'map'))) : nil
96 115 full_link = display?(asset, :full) ? (display == 'full' ? _('Full') : link_to(_('Full'), params.merge(:display => 'full'))) : nil
97   - content_tag('div',
  116 + content_tag('div',
98 117 content_tag('strong', _('Display')) + ': ' + [compact_link, map_link, full_link].compact.join(' | ').html_safe,
99 118 :class => 'search-customize-options'
100 119 )
101 120 end
102 121 end
103 122  
104   - def filter_selector(asset, filter, float = 'right')
  123 + def filters(asset)
  124 + return if !asset
105 125 klass = asset_class(asset)
106   - if klass::SEARCH_FILTERS.count > 1
107   - options = options_for_select(klass::SEARCH_FILTERS.map {|f| [FILTER_TRANSLATION[f], f]}, filter)
108   - url_params = url_for(params.merge(:filter => 'FILTER'))
109   - onchange = "document.location.href = '#{url_params}'.replace('FILTER', this.value)"
110   - select_field = select_tag(:filter, options, :onchange => onchange)
111   - content_tag('div',
112   - content_tag('strong', _('Filter')) + ': ' + select_field,
113   - :class => "search-customize-options"
114   - )
115   - end
  126 + content_tag('div', klass::SEARCH_FILTERS.map do |name, options|
  127 + default = klass.respond_to?("default_search_#{name}") ? klass.send("default_search_#{name}".to_s) : nil
  128 + select_filter(name, options, default)
  129 + end.join("\n"), :id => 'search-filters')
  130 + end
  131 +
  132 + def assets_menu(selected)
  133 + assets = @enabled_searches.keys
  134 + # Events is a search asset but do not have a good interface for
  135 + #TODO searching. When this is solved we may add it back again to the assets
  136 + # menu.
  137 + assets.delete(:events)
  138 + content_tag('ul',
  139 + assets.map do |asset|
  140 + options = {}
  141 + options.merge!(:class => 'selected') if selected.to_s == asset.to_s
  142 + content_tag('li', asset_link(asset), options)
  143 + end.join("\n"),
  144 + :id => 'assets-menu')
116 145 end
117 146  
118   - def filter_title(asset, filter)
119   - {
120   - 'articles_more_recent' => _('More recent contents from network'),
121   - 'articles_more_popular' => _('More viewed contents from network'),
122   - 'articles_more_comments' => _('Most commented contents from network'),
123   - 'people_more_recent' => _('More recent people from network'),
124   - 'people_more_active' => _('More active people from network'),
125   - 'people_more_popular' => _('More popular people from network'),
126   - 'communities_more_recent' => _('More recent communities from network'),
127   - 'communities_more_active' => _('More active communities from network'),
128   - 'communities_more_popular' => _('More popular communities from network'),
129   - 'enterprises_more_recent' => _('More recent enterprises from network'),
130   - 'enterprises_more_active' => _('More active enterprises from network'),
131   - 'enterprises_more_popular' => _('More popular enterprises from network'),
132   - 'products_more_recent' => _('Highlights'),
133   - }[asset.to_s + '_' + filter].to_s
  147 + def asset_link(asset)
  148 + link_to(@enabled_searches[asset], "/search/#{asset}")
134 149 end
135 150  
136 151 end
... ...
app/helpers/search_term_helper.rb 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +module SearchTermHelper
  2 + def register_search_term(term, total, indexed, context, asset='all')
  3 + normalized_term = normalize_term(term)
  4 + if normalized_term.present?
  5 + search_term = SearchTerm.find_or_create(normalized_term, context, asset)
  6 + SearchTermOccurrence.create!(:search_term => search_term, :total => total, :indexed => indexed)
  7 + end
  8 + end
  9 +
  10 + #FIXME For some reason the job is created but nothing is ran.
  11 + #handle_asynchronously :register_search_term
  12 +
  13 + #TODO Think smarter criteria to normalize search terms properly
  14 + def normalize_term(search_term)
  15 + search_term ||= ''
  16 + search_term.downcase
  17 + end
  18 +end
... ...
app/helpers/sweeper_helper.rb
... ... @@ -56,12 +56,12 @@ module SweeperHelper
56 56 if profile
57 57 profile.blocks.each {|block|
58 58 conditions = block.class.expire_on
59   - blocks_to_expire << block unless (conditions[:profile] & causes).empty?
  59 + blocks_to_expire << block unless (conditions[:profile] & causes).blank?
60 60 }
61 61 end
62 62 environment.blocks.each {|block|
63 63 conditions = block.class.expire_on
64   - blocks_to_expire << block unless (conditions[:environment] & causes).empty?
  64 + blocks_to_expire << block unless (conditions[:environment] & causes).blank?
65 65 }
66 66  
67 67 blocks_to_expire.uniq!
... ...
app/helpers/tinymce_helper.rb 0 → 100644
... ... @@ -0,0 +1,51 @@
  1 +module TinymceHelper
  2 + include MacrosHelper
  3 +
  4 + def tinymce_js
  5 + output = ''
  6 + output += javascript_include_tag 'tinymce/js/tinymce/tinymce.min.js'
  7 + output += javascript_include_tag 'tinymce/js/tinymce/jquery.tinymce.min.js'
  8 + output += javascript_include_tag 'tinymce.js'
  9 + output += include_macro_js_files.to_s
  10 + output
  11 + end
  12 +
  13 + def tinymce_init_js options = {}
  14 + options.merge! :document_base_url => top_url,
  15 + :content_css => "/stylesheets/tinymce.css,#{macro_css_files}",
  16 + :plugins => %w[compat3x advlist autolink lists link image charmap print preview hr anchor pagebreak
  17 + searchreplace wordcount visualblocks visualchars code fullscreen
  18 + insertdatetime media nonbreaking save table contextmenu directionality
  19 + emoticons template paste textcolor colorpicker textpattern],
  20 + :language => tinymce_language
  21 +
  22 + options[:toolbar1] = "insertfile undo redo | copy paste | bold italic underline | styleselect fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image"
  23 + if options[:mode] == 'simple'
  24 + options[:menubar] = false
  25 + else
  26 + options[:menubar] = 'edit insert view tools'
  27 + options[:toolbar2] = 'print preview code media | table'
  28 +
  29 + options[:toolbar2] += ' | macros'
  30 + macros_with_buttons.each do |macro|
  31 + options[:toolbar2] += " #{macro.identifier}"
  32 + end
  33 + end
  34 +
  35 + options[:macros_setup] = macros_with_buttons.map do |macro|
  36 + <<-EOS
  37 + ed.addButton('#{macro.identifier}', {
  38 + title: #{macro_title(macro).to_json},
  39 + onclick: #{generate_macro_config_dialog macro},
  40 + image : '#{macro.configuration[:icon_path]}'
  41 + });
  42 + EOS
  43 + end
  44 +
  45 + #cleanup non tinymce options
  46 + options = options.except :mode
  47 +
  48 + "noosfero.tinymce.init(#{options.to_json})"
  49 + end
  50 +
  51 +end
... ...
app/helpers/token_helper.rb
... ... @@ -12,12 +12,14 @@ module TokenHelper
12 12 options[:search_delay] ||= 1000
13 13 options[:prevent_duplicates] ||= true
14 14 options[:backspace_delete_item] ||= false
  15 + options[:zindex] ||= 999
15 16 options[:focus] ||= false
16 17 options[:avoid_enter] ||= true
17 18 options[:on_result] ||= 'null'
18 19 options[:on_add] ||= 'null'
19 20 options[:on_delete] ||= 'null'
20 21 options[:on_ready] ||= 'null'
  22 + options[:query_param] ||= 'q'
21 23  
22 24 result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id})))
23 25 result += javascript_tag("jQuery('##{element_id}')
... ... @@ -30,7 +32,8 @@ module TokenHelper
30 32 searchDelay: #{options[:search_delay].to_json},
31 33 preventDuplicates: #{options[:prevent_duplicates].to_json},
32 34 backspaceDeleteItem: #{options[:backspace_delete_item].to_json},
33   - queryParam: #{name.to_json},
  35 + zindex: #{options[:zindex].to_json},
  36 + queryParam: #{options[:query_param].to_json},
34 37 tokenLimit: #{options[:token_limit].to_json},
35 38 onResult: #{options[:on_result]},
36 39 onAdd: #{options[:on_add]},
... ... @@ -48,4 +51,4 @@ module TokenHelper
48 51 result
49 52 end
50 53  
51   -end
52 54 \ No newline at end of file
  55 +end
... ...
app/mailers/user_mailer.rb
... ... @@ -41,6 +41,23 @@ class UserMailer &lt; ActionMailer::Base
41 41 )
42 42 end
43 43  
  44 + def profiles_suggestions_email(user)
  45 + @recipient = user.name
  46 + @environment = user.environment.name
  47 + @url = user.environment.top_url
  48 + @people_suggestions_url = user.people_suggestions_url
  49 + @people_suggestions = user.suggested_people.sample(3)
  50 + @communities_suggestions_url = user.communities_suggestions_url
  51 + @communities_suggestions = user.suggested_communities.sample(3)
  52 +
  53 + mail(
  54 + content_type: 'text/html',
  55 + to: user.email,
  56 + from: "#{user.environment.name} <#{user.environment.contact_email}>",
  57 + subject: _("[%s] What about grow up your network?") % user.environment.name
  58 + )
  59 + end
  60 +
44 61 class Job < Struct.new(:user, :method)
45 62 def perform
46 63 UserMailer.send(method, user).deliver
... ...
app/models/add_friend.rb
... ... @@ -14,6 +14,11 @@ class AddFriend &lt; Task
14 14 alias :friend :target
15 15 alias :friend= :target=
16 16  
  17 + after_create do |task|
  18 + TaskMailer.invitation_notification(task).deliver unless task.friend
  19 + remove_from_suggestion_list(task)
  20 + end
  21 +
17 22 def perform
18 23 target.add_friend(requestor, group_for_friend)
19 24 requestor.add_friend(target, group_for_person)
... ... @@ -48,4 +53,8 @@ class AddFriend &lt; Task
48 53 {:type => :profile_image, :profile => requestor, :url => requestor.url}
49 54 end
50 55  
  56 + def remove_from_suggestion_list(task)
  57 + suggestion = task.requestor.profile_suggestions.find_by_suggestion_id task.target.id
  58 + suggestion.disable if suggestion
  59 + end
51 60 end
... ...
app/models/add_member.rb
... ... @@ -10,6 +10,10 @@ class AddMember &lt; Task
10 10  
11 11 settings_items :roles
12 12  
  13 + after_create do |task|
  14 + remove_from_suggestion_list(task)
  15 + end
  16 +
13 17 def perform
14 18 if !self.roles or (self.roles.uniq.compact.length == 1 and self.roles.uniq.compact.first.to_i.zero?)
15 19 self.roles = [Profile::Roles.member(organization.environment.id).id]
... ... @@ -46,4 +50,9 @@ class AddMember &lt; Task
46 50 _('You will need login to %{system} in order to accept or reject %{requestor} as a member of %{organization}.') % { :system => target.environment.name, :requestor => requestor.name, :organization => organization.name }
47 51 end
48 52  
  53 + def remove_from_suggestion_list(task)
  54 + suggestion = task.requestor.profile_suggestions.find_by_suggestion_id task.target.id
  55 + suggestion.disable if suggestion
  56 + end
  57 +
49 58 end
... ...
app/models/approve_article.rb
... ... @@ -22,6 +22,7 @@ class ApproveArticle &lt; Task
22 22 end
23 23  
24 24 settings_items :closing_statment, :article_parent_id, :highlighted
  25 + settings_items :create_link, :type => :boolean, :default => false
25 26  
26 27 def article_parent
27 28 Article.find_by_id article_parent_id.to_i
... ... @@ -48,7 +49,11 @@ class ApproveArticle &lt; Task
48 49 end
49 50  
50 51 def perform
51   - article.copy!(:name => name, :abstract => abstract, :body => body, :profile => target, :reference_article => article, :parent => article_parent, :highlighted => highlighted, :source => article.source, :last_changed_by_id => article.last_changed_by_id, :created_by_id => article.created_by_id)
  52 + if create_link
  53 + LinkArticle.create!(:reference_article => article, :profile => target, :parent => article_parent, :highlighted => highlighted)
  54 + else
  55 + article.copy!(:name => name, :abstract => abstract, :body => body, :profile => target, :reference_article => article, :parent => article_parent, :highlighted => highlighted, :source => article.source, :last_changed_by_id => article.last_changed_by_id, :created_by_id => article.created_by_id)
  56 + end
52 57 end
53 58  
54 59 def title
... ...
app/models/article.rb
... ... @@ -2,25 +2,29 @@ require &#39;hpricot&#39;
2 2  
3 3 class Article < ActiveRecord::Base
4 4  
5   - attr_accessible :name, :body, :abstract, :profile, :tag_list, :parent, :allow_members_to_edit, :translation_of_id, :language, :license_id, :parent_id, :display_posts_in_current_language, :category_ids, :posts_per_page, :moderate_comments, :accept_comments, :feed, :published, :source, :highlighted, :notify_comments, :display_hits, :slug, :external_feed_builder, :display_versions, :external_link, :image_builder
  5 + attr_accessible :name, :body, :abstract, :profile, :tag_list, :parent,
  6 + :allow_members_to_edit, :translation_of_id, :language,
  7 + :license_id, :parent_id, :display_posts_in_current_language,
  8 + :category_ids, :posts_per_page, :moderate_comments,
  9 + :accept_comments, :feed, :published, :source,
  10 + :highlighted, :notify_comments, :display_hits, :slug,
  11 + :external_feed_builder, :display_versions, :external_link,
  12 + :image_builder, :show_to_followers
6 13  
7 14 acts_as_having_image
8 15  
9 16 SEARCHABLE_FIELDS = {
10   - :name => 10,
11   - :abstract => 3,
12   - :body => 2,
13   - :slug => 1,
14   - :filename => 1,
  17 + :name => {:label => _('Name'), :weight => 10},
  18 + :abstract => {:label => _('Abstract'), :weight => 3},
  19 + :body => {:label => _('Content'), :weight => 2},
  20 + :slug => {:label => _('Slug'), :weight => 1},
  21 + :filename => {:label => _('Filename'), :weight => 1},
15 22 }
16 23  
17   - SEARCH_FILTERS = %w[
18   - more_recent
19   - more_popular
20   - more_comments
21   - ]
22   -
23   - SEARCH_DISPLAYS = %w[full]
  24 + SEARCH_FILTERS = {
  25 + :order => %w[more_recent more_popular more_comments],
  26 + :display => %w[full]
  27 + }
24 28  
25 29 def self.default_search_display
26 30 'full'
... ... @@ -103,6 +107,11 @@ class Article &lt; ActiveRecord::Base
103 107 self.activity.destroy if self.activity
104 108 end
105 109  
  110 + after_destroy :destroy_link_article
  111 + def destroy_link_article
  112 + Article.where(:reference_article_id => self.id, :type => LinkArticle).destroy_all
  113 + end
  114 +
106 115 xss_terminate :only => [ :name ], :on => 'validation', :with => 'white_list'
107 116  
108 117 scope :in_category, lambda { |category|
... ... @@ -157,14 +166,17 @@ class Article &lt; ActiveRecord::Base
157 166 self.profile
158 167 end
159 168  
160   - def self.human_attribute_name(attrib, options = {})
  169 + def self.human_attribute_name_with_customization(attrib, options={})
161 170 case attrib.to_sym
162 171 when :name
163 172 _('Title')
164 173 else
165   - _(self.superclass.human_attribute_name(attrib))
  174 + _(self.human_attribute_name_without_customization(attrib))
166 175 end
167 176 end
  177 + class << self
  178 + alias_method_chain :human_attribute_name, :customization
  179 + end
168 180  
169 181 def css_class_list
170 182 [self.class.name.to_css_class]
... ... @@ -282,13 +294,6 @@ class Article &lt; ActiveRecord::Base
282 294 end
283 295 end
284 296  
285   - def reported_version(options = {})
286   - article = self
287   - search_path = Rails.root.join('app', 'views', 'shared', 'reported_versions')
288   - partial_path = File.join('shared', 'reported_versions', partial_for_class_in_view_path(article.class, search_path))
289   - lambda { render_to_string(:partial => partial_path, :locals => {:article => article}) }
290   - end
291   -
292 297 # returns the data of the article. Must be overriden in each subclass to
293 298 # provide the correct content for the article.
294 299 def data
... ... @@ -337,7 +342,7 @@ class Article &lt; ActiveRecord::Base
337 342 def belongs_to_blog?
338 343 self.parent and self.parent.blog?
339 344 end
340   -
  345 +
341 346 def belongs_to_forum?
342 347 self.parent and self.parent.forum?
343 348 end
... ... @@ -449,6 +454,7 @@ class Article &lt; ActiveRecord::Base
449 454 if self.parent && !self.parent.published?
450 455 return false
451 456 end
  457 +
452 458 true
453 459 else
454 460 false
... ... @@ -468,7 +474,9 @@ class Article &lt; ActiveRecord::Base
468 474 scope :no_folders, lambda {|profile|{:conditions => ['articles.type NOT IN (?)', profile.folder_types]}}
469 475 scope :galleries, :conditions => [ "articles.type IN ('Gallery')" ]
470 476 scope :images, :conditions => { :is_image => true }
  477 + scope :no_images, :conditions => { :is_image => false }
471 478 scope :text_articles, :conditions => [ 'articles.type IN (?)', text_article_types ]
  479 + scope :files, :conditions => { :type => 'UploadedFile' }
472 480 scope :with_types, lambda { |types| { :conditions => [ 'articles.type IN (?)', types ] } }
473 481  
474 482 scope :more_popular, :order => 'hits DESC'
... ... @@ -480,14 +488,17 @@ class Article &lt; ActiveRecord::Base
480 488 {:conditions => [" articles.published = ? OR
481 489 articles.last_changed_by_id = ? OR
482 490 articles.profile_id = ? OR
483   - ?",
484   - true, user.id, user.id, user.has_permission?(:view_private_content, profile)] }
  491 + ? OR articles.show_to_followers = ? AND ?",
  492 + true, user.id, user.id, user.has_permission?(:view_private_content, profile),
  493 + true, user.follows?(profile)]}
485 494 end
486 495  
  496 +
487 497 def display_unpublished_article_to?(user)
488 498 user == author || allow_view_private_content?(user) || user == profile ||
489 499 user.is_admin?(profile.environment) || user.is_admin?(profile) ||
490   - article_privacy_exceptions.include?(user)
  500 + article_privacy_exceptions.include?(user) ||
  501 + (self.show_to_followers && user.follows?(profile))
491 502 end
492 503  
493 504 def display_to?(user = nil)
... ... @@ -516,7 +527,10 @@ class Article &lt; ActiveRecord::Base
516 527 end
517 528  
518 529 alias :allow_delete? :allow_post_content?
519   - alias :allow_spread? :allow_post_content?
  530 +
  531 + def allow_spread?(user = nil)
  532 + user && public?
  533 + end
520 534  
521 535 def allow_create?(user)
522 536 allow_post_content?(user) || allow_publish_content?(user)
... ...
app/models/block.rb
1 1 class Block < ActiveRecord::Base
2 2  
3   - attr_accessible :title, :display, :limit, :box_id, :posts_per_page, :visualization_format, :language, :display_user, :box
  3 + attr_accessible :title, :display, :limit, :box_id, :posts_per_page, :visualization_format, :language, :display_user, :box, :fixed
4 4  
5 5 # to be able to generate HTML
6 6 include ActionView::Helpers::UrlHelper
... ... @@ -64,7 +64,7 @@ class Block &lt; ActiveRecord::Base
64 64 end
65 65  
66 66 def display_to_user?(user)
67   - display_user == 'all' || (user.nil? && display_user == 'not_logged') || (user && display_user == 'logged')
  67 + display_user == 'all' || (user.nil? && display_user == 'not_logged') || (user && display_user == 'logged') || (user && display_user == 'followers' && user.follows?(owner))
68 68 end
69 69  
70 70 def display_always(context)
... ... @@ -75,7 +75,7 @@ class Block &lt; ActiveRecord::Base
75 75 if context[:article]
76 76 return context[:article] == owner.home_page
77 77 else
78   - return context[:request_path] == '/'
  78 + return home_page_path?(context[:request_path])
79 79 end
80 80 end
81 81  
... ... @@ -83,7 +83,7 @@ class Block &lt; ActiveRecord::Base
83 83 if context[:article]
84 84 return context[:article] != owner.home_page
85 85 else
86   - return context[:request_path] != '/' + (owner.kind_of?(Profile) ? owner.identifier : '')
  86 + return !home_page_path?(context[:request_path])
87 87 end
88 88 end
89 89  
... ... @@ -110,11 +110,14 @@ class Block &lt; ActiveRecord::Base
110 110 # * <tt>'all'</tt>: the block is always displayed
111 111 settings_items :language, :type => :string, :default => 'all'
112 112  
  113 + # The block can be configured to be fixed. Only can be edited by environment admins
  114 + settings_items :fixed, :type => :boolean, :default => false
  115 +
113 116 # returns the description of the block, used when the user sees a list of
114 117 # blocks to choose one to include in the design.
115 118 #
116 119 # Must be redefined in subclasses to match the description of each block
117   - # type.
  120 + # type.
118 121 def self.description
119 122 '(dummy)'
120 123 end
... ... @@ -124,13 +127,13 @@ class Block &lt; ActiveRecord::Base
124 127 # This method can return several types of objects:
125 128 #
126 129 # * <tt>String</tt>: if the string starts with <tt>http://</tt> or <tt>https://</tt>, then it is assumed to be address of an IFRAME. Otherwise it's is used as regular HTML.
127   - # * <tt>Hash</tt>: the hash is used to build an URL that is used as the address for a IFRAME.
  130 + # * <tt>Hash</tt>: the hash is used to build an URL that is used as the address for a IFRAME.
128 131 # * <tt>Proc</tt>: the Proc is evaluated in the scope of BoxesHelper. The
129 132 # block can then use <tt>render</tt>, <tt>link_to</tt>, etc.
130 133 #
131 134 # The method can also return <tt>nil</tt>, which means "no content".
132 135 #
133   - # See BoxesHelper#extract_block_content for implementation details.
  136 + # See BoxesHelper#extract_block_content for implementation details.
134 137 def content(args={})
135 138 "This is block number %d" % self.id
136 139 end
... ... @@ -192,7 +195,7 @@ class Block &lt; ActiveRecord::Base
192 195  
193 196 # Override in your subclasses.
194 197 # Define which events and context should cause the block cache to expire
195   - # Possible events are: :article, :profile, :friendship, :category
  198 + # Possible events are: :article, :profile, :friendship, :category, :role_assignment
196 199 # Possible contexts are: :profile, :environment
197 200 def self.expire_on
198 201 {
... ... @@ -221,6 +224,7 @@ class Block &lt; ActiveRecord::Base
221 224 'all' => _('All users'),
222 225 'logged' => _('Logged'),
223 226 'not_logged' => _('Not logged'),
  227 + 'followers' => owner.class != Environment && owner.organization? ? _('Members') : _('Friends')
224 228 }
225 229 end
226 230  
... ... @@ -234,4 +238,26 @@ class Block &lt; ActiveRecord::Base
234 238 duplicated_block
235 239 end
236 240  
  241 + def copy_from(block)
  242 + self.settings = block.settings
  243 + self.position = block.position
  244 + end
  245 +
  246 + private
  247 +
  248 + def home_page_path
  249 + home_page_url = Noosfero.root('/')
  250 +
  251 + if owner.kind_of?(Profile)
  252 + home_page_url += "profile/" if owner.home_page.nil?
  253 + home_page_url += owner.identifier
  254 + end
  255 +
  256 + return home_page_url
  257 + end
  258 +
  259 + def home_page_path? path
  260 + return path == home_page_path || path == (home_page_path + '/')
  261 + end
  262 +
237 263 end
... ...
app/models/blog.rb
... ... @@ -53,7 +53,7 @@ class Blog &lt; Folder
53 53 def prepare_external_feed
54 54 unless self.external_feed_data.nil?
55 55 if self.external_feed(true) && self.external_feed.id == self.external_feed_data[:id].to_i
56   - self.external_feed.attributes = self.external_feed_data
  56 + self.external_feed.attributes = self.external_feed_data.except(:id)
57 57 else
58 58 self.build_external_feed(self.external_feed_data, :without_protection => true)
59 59 end
... ...
app/models/box.rb
... ... @@ -28,9 +28,6 @@ class Box &lt; ActiveRecord::Base
28 28 CategoriesBlock,
29 29 CommunitiesBlock,
30 30 EnterprisesBlock,
31   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
32   - # the Noosfero core soon, see ActionItem3045
33   - EnvironmentStatisticsBlock,
34 31 FansBlock,
35 32 FavoriteEnterprisesBlock,
36 33 FeedReaderBlock,
... ... @@ -53,9 +50,6 @@ class Box &lt; ActiveRecord::Base
53 50 CommunitiesBlock,
54 51 DisabledEnterpriseMessageBlock,
55 52 EnterprisesBlock,
56   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
57   - # the Noosfero core soon, see ActionItem3045
58   - EnvironmentStatisticsBlock,
59 53 FansBlock,
60 54 FavoriteEnterprisesBlock,
61 55 FeaturedProductsBlock,
... ...
app/models/category.rb
... ... @@ -3,10 +3,10 @@ class Category &lt; ActiveRecord::Base
3 3 attr_accessible :name, :parent_id, :display_color, :display_in_menu, :image_builder, :environment, :parent
4 4  
5 5 SEARCHABLE_FIELDS = {
6   - :name => 10,
7   - :acronym => 5,
8   - :abbreviation => 5,
9   - :slug => 1,
  6 + :name => {:label => _('Name'), :weight => 10},
  7 + :acronym => {:label => _('Acronym'), :weight => 5},
  8 + :abbreviation => {:label => _('Abbreviation'), :weight => 5},
  9 + :slug => {:label => _('Slug'), :weight => 1},
10 10 }
11 11  
12 12 validates_exclusion_of :slug, :in => [ 'index' ], :message => N_('{fn} cannot be like that.').fix_i18n
... ... @@ -14,9 +14,6 @@ class Category &lt; ActiveRecord::Base
14 14 validates_uniqueness_of :slug,:scope => [ :environment_id, :parent_id ], :message => N_('{fn} is already being used by another category.').fix_i18n
15 15 belongs_to :environment
16 16  
17   - validates_inclusion_of :display_color, :in => 1..15, :allow_nil => true
18   - validates_uniqueness_of :display_color, :scope => :environment_id, :if => (lambda { |cat| ! cat.display_color.nil? }), :message => N_('{fn} was already assigned to another category.').fix_i18n
19   -
20 17 # Finds all top level categories for a given environment.
21 18 scope :top_level_for, lambda { |environment|
22 19 {:conditions => ['parent_id is null and environment_id = ?', environment.id ]}
... ... @@ -42,6 +39,13 @@ class Category &lt; ActiveRecord::Base
42 39  
43 40 acts_as_having_image
44 41  
  42 + before_save :normalize_display_color
  43 +
  44 + def normalize_display_color
  45 + display_color.gsub!('#', '') if display_color
  46 + display_color = nil if display_color.blank?
  47 + end
  48 +
45 49 scope :from_types, lambda { |types|
46 50 types.select{ |t| t.blank? }.empty? ?
47 51 { :conditions => { :type => types } } :
... ... @@ -101,4 +105,12 @@ class Category &lt; ActiveRecord::Base
101 105 self.children.find(:all, :conditions => {:display_in_menu => true}).empty?
102 106 end
103 107  
  108 + def with_color
  109 + if display_color.blank?
  110 + parent.nil? ? nil : parent.with_color
  111 + else
  112 + self
  113 + end
  114 + end
  115 +
104 116 end
... ...
app/models/certifier.rb
... ... @@ -3,9 +3,9 @@ class Certifier &lt; ActiveRecord::Base
3 3 attr_accessible :name, :environment
4 4  
5 5 SEARCHABLE_FIELDS = {
6   - :name => 10,
7   - :description => 3,
8   - :link => 1,
  6 + :name => {:label => _('Name'), :weight => 10},
  7 + :description => {:label => _('Description'), :weight => 3},
  8 + :link => {:label => _('Link'), :weight => 1},
9 9 }
10 10  
11 11 belongs_to :environment
... ...
app/models/change_password.rb
... ... @@ -2,16 +2,19 @@ class ChangePassword &lt; Task
2 2  
3 3 attr_accessor :password, :password_confirmation
4 4  
5   - def self.human_attribute_name(attrib, options = {})
  5 + def self.human_attribute_name_with_customization(attrib, options={})
6 6 case attrib.to_sym
7 7 when :password
8 8 _('Password')
9 9 when :password_confirmation
10 10 _('Password Confirmation')
11 11 else
12   - _(self.superclass.human_attribute_name(attrib))
  12 + _(self.human_attribute_name_without_customization(attrib))
13 13 end
14 14 end
  15 + class << self
  16 + alias_method_chain :human_attribute_name, :customization
  17 + end
15 18  
16 19 validates_presence_of :requestor
17 20  
... ...
app/models/comment.rb
1 1 class Comment < ActiveRecord::Base
2 2  
3 3 SEARCHABLE_FIELDS = {
4   - :title => 10,
5   - :name => 4,
6   - :body => 2,
  4 + :title => {:label => _('Title'), :weight => 10},
  5 + :name => {:label => _('Name'), :weight => 4},
  6 + :body => {:label => _('Content'), :weight => 2},
7 7 }
8 8  
9 9 attr_accessible :body, :author, :name, :email, :title, :reply_of_id, :source
... ...
app/models/communities_block.rb
... ... @@ -14,19 +14,17 @@ class CommunitiesBlock &lt; ProfileListBlock
14 14 _('This block displays the communities in which the user is a member.')
15 15 end
16 16  
  17 + def suggestions
  18 + return nil unless owner.kind_of?(Profile)
  19 + owner.profile_suggestions.of_community.enabled.limit(3).includes(:suggestion)
  20 + end
  21 +
17 22 def footer
18 23 owner = self.owner
19   - case owner
20   - when Profile
21   - lambda do |context|
22   - link_to s_('communities|View all'), :profile => owner.identifier, :controller => 'profile', :action => 'communities'
23   - end
24   - when Environment
25   - lambda do |context|
26   - link_to s_('communities|View all'), :controller => 'search', :action => 'communities'
27   - end
28   - else
29   - ''
  24 + suggestions = self.suggestions
  25 + return '' unless owner.kind_of?(Profile) || owner.kind_of?(Environment)
  26 + proc do
  27 + render :file => 'blocks/communities', :locals => { :owner => owner, :suggestions => suggestions }
30 28 end
31 29 end
32 30  
... ...
app/models/community.rb
... ... @@ -50,16 +50,6 @@ class Community &lt; Organization
50 50 super + FIELDS
51 51 end
52 52  
53   - validate :presence_of_required_fieds
54   -
55   - def presence_of_required_fieds
56   - self.required_fields.each do |field|
57   - if self.send(field).blank?
58   - self.errors.add_on_blank(field)
59   - end
60   - end
61   - end
62   -
63 53 def active_fields
64 54 environment ? environment.active_community_fields : []
65 55 end
... ... @@ -78,7 +68,7 @@ class Community &lt; Organization
78 68 end
79 69  
80 70 def default_template
81   - environment.community_template
  71 + environment.community_default_template
82 72 end
83 73  
84 74 def news(limit = 30, highlight = false)
... ...
app/models/create_enterprise.rb
... ... @@ -73,7 +73,13 @@ class CreateEnterprise &lt; Task
73 73  
74 74 # sets the associated region for the enterprise creation
75 75 def region=(value)
76   - raise ArgumentError.new("Region expected, but got #{value.class}") unless value.kind_of?(Region)
  76 + unless value.kind_of?(Region)
  77 + begin
  78 + value = Region.find(value)
  79 + rescue
  80 + raise ArgumentError.new("Could not find any region with the id #{value}")
  81 + end
  82 + end
77 83  
78 84 @region = value
79 85 self.region_id = value.id
... ...
app/models/domain.rb
... ... @@ -92,4 +92,11 @@ class Domain &lt; ActiveRecord::Base
92 92 @hosting = {}
93 93 end
94 94  
  95 + # Detects a domain's custom text domain chain if available based on a domain
  96 + # served on multitenancy configuration or a registered domain.
  97 + def self.custom_locale(domainname)
  98 + domain = Noosfero::MultiTenancy.mapping[domainname] || domainname[/(.*?)\./,1]
  99 + FastGettext.translation_repositories.keys.include?(domain) ? domain : FastGettext.default_text_domain
  100 + end
  101 +
95 102 end
... ...
app/models/enterprise.rb
... ... @@ -4,7 +4,10 @@ class Enterprise &lt; Organization
4 4  
5 5 attr_accessible :business_name, :address_reference, :district, :tag_list, :organization_website, :historic_and_current_context, :activities_short_description, :products_per_catalog_page
6 6  
7   - SEARCH_DISPLAYS += %w[map full]
  7 + SEARCH_FILTERS = {
  8 + :order => %w[more_recent more_popular more_active],
  9 + :display => %w[compact full map]
  10 + }
8 11  
9 12 def self.type_name
10 13 _('Enterprise')
... ... @@ -59,16 +62,6 @@ class Enterprise &lt; Organization
59 62 super + FIELDS
60 63 end
61 64  
62   - validate :presence_of_required_fieds
63   -
64   - def presence_of_required_fieds
65   - self.required_fields.each do |field|
66   - if self.send(field).blank?
67   - self.errors.add_on_blank(field)
68   - end
69   - end
70   - end
71   -
72 65 def active_fields
73 66 environment ? environment.active_enterprise_fields : []
74 67 end
... ... @@ -107,7 +100,12 @@ class Enterprise &lt; Organization
107 100 self.tasks.where(:type => 'EnterpriseActivation').first
108 101 end
109 102  
110   - def enable(owner)
  103 + def enable(owner = nil)
  104 + if owner.nil?
  105 + self.visible = true
  106 + return self.save
  107 + end
  108 +
111 109 return if enabled
112 110 # must be set first for the following to work
113 111 self.enabled = true
... ... @@ -169,7 +167,7 @@ class Enterprise &lt; Organization
169 167 end
170 168  
171 169 def default_template
172   - environment.enterprise_template
  170 + environment.enterprise_default_template
173 171 end
174 172  
175 173 def template_with_inactive_enterprise
... ...
app/models/environment.rb
... ... @@ -3,13 +3,14 @@
3 3 # domains.
4 4 class Environment < ActiveRecord::Base
5 5  
6   - attr_accessible :name, :is_default, :signup_welcome_text_subject, :signup_welcome_text_body, :terms_of_use, :message_for_disabled_enterprise, :news_amount_by_folder, :default_language, :languages, :description, :organization_approval_method, :enabled_plugins, :enabled_features, :redirection_after_login, :redirection_after_signup, :contact_email, :theme, :reports_lower_bound, :noreply_email, :signup_welcome_screen_body
  6 + attr_accessible :name, :is_default, :signup_welcome_text_subject, :signup_welcome_text_body, :terms_of_use, :message_for_disabled_enterprise, :news_amount_by_folder, :default_language, :languages, :description, :organization_approval_method, :enabled_plugins, :enabled_features, :redirection_after_login, :redirection_after_signup, :contact_email, :theme, :reports_lower_bound, :noreply_email, :signup_welcome_screen_body, :members_whitelist_enabled, :members_whitelist
7 7  
8 8 has_many :users
9 9  
10 10 self.partial_updates = false
11 11  
12 12 has_many :tasks, :dependent => :destroy, :as => 'target'
  13 + has_many :search_terms, :as => :context
13 14  
14 15 IDENTIFY_SCRIPTS = /(php[0-9s]?|[sp]htm[l]?|pl|py|cgi|rb)/
15 16  
... ... @@ -85,7 +86,9 @@ class Environment &lt; ActiveRecord::Base
85 86 end
86 87  
87 88 def admins
88   - Person.members_of(self).all(:conditions => ['role_assignments.role_id = ?', Environment::Roles.admin(self).id])
  89 + admin_role = Environment::Roles.admin(self)
  90 + return [] if admin_role.blank?
  91 + Person.members_of(self).all(:conditions => ['role_assignments.role_id = ?', admin_role.id])
89 92 end
90 93  
91 94 # returns the available features for a Environment, in the form of a
... ... @@ -124,6 +127,7 @@ class Environment &lt; ActiveRecord::Base
124 127 'organizations_are_moderated_by_default' => _("Organizations have moderated publication by default"),
125 128 'enable_organization_url_change' => _("Allow organizations to change their URL"),
126 129 'admin_must_approve_new_communities' => _("Admin must approve creation of communities"),
  130 + 'admin_must_approve_new_users' => _("Admin must approve registration of new users"),
127 131 'show_balloon_with_profile_links_when_clicked' => _('Show a balloon with profile links when a profile image is clicked'),
128 132 'xmpp_chat' => _('XMPP/Jabber based chat'),
129 133 'show_zoom_button_on_article_images' => _('Show a zoom link on all article images'),
... ... @@ -132,7 +136,8 @@ class Environment &lt; ActiveRecord::Base
132 136 'send_welcome_email_to_new_users' => _('Send welcome e-mail to new users'),
133 137 'allow_change_of_redirection_after_login' => _('Allow users to set the page to redirect after login'),
134 138 'display_my_communities_on_user_menu' => _('Display on menu the list of communities the user can manage'),
135   - 'display_my_enterprises_on_user_menu' => _('Display on menu the list of enterprises the user can manage')
  139 + 'display_my_enterprises_on_user_menu' => _('Display on menu the list of enterprises the user can manage'),
  140 + 'restrict_to_members' => _('Show content only to members')
136 141 }
137 142 end
138 143  
... ... @@ -153,7 +158,8 @@ class Environment &lt; ActiveRecord::Base
153 158 'site_homepage' => _('Redirects the user to the environment homepage.'),
154 159 'user_profile_page' => _('Redirects the user to his profile page.'),
155 160 'user_homepage' => _('Redirects the user to his homepage.'),
156   - 'user_control_panel' => _('Redirects the user to his control panel.')
  161 + 'user_control_panel' => _('Redirects the user to his control panel.'),
  162 + 'welcome_page' => _('Redirects the user to the environment welcome page.')
157 163 }
158 164 end
159 165 validates_inclusion_of :redirection_after_signup, :in => Environment.signup_redirection_options.keys, :allow_nil => true
... ... @@ -175,9 +181,6 @@ class Environment &lt; ActiveRecord::Base
175 181  
176 182 # "left" area
177 183 env.boxes[1].blocks << LoginBlock.new
178   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
179   - # the Noosfero core soon, see ActionItem3045
180   - env.boxes[1].blocks << EnvironmentStatisticsBlock.new
181 184 env.boxes[1].blocks << RecentDocumentsBlock.new
182 185  
183 186 # "right" area
... ... @@ -284,6 +287,7 @@ class Environment &lt; ActiveRecord::Base
284 287 www.flickr.com
285 288 www.gmodules.com
286 289 www.youtube.com
  290 + openstreetmap.org
287 291 ] + ('a' .. 'z').map{|i| "#{i}.yimg.com"}
288 292  
289 293 settings_items :enabled_plugins, :type => Array, :default => Noosfero::Plugin.available_plugin_names
... ... @@ -303,6 +307,17 @@ class Environment &lt; ActiveRecord::Base
303 307 settings[:signup_welcome_screen_body].present?
304 308 end
305 309  
  310 + settings_items :members_whitelist_enabled, :type => :boolean, :default => false
  311 + settings_items :members_whitelist, :type => Array, :default => []
  312 +
  313 + def in_whitelist?(person)
  314 + !members_whitelist_enabled || members_whitelist.include?(person.id)
  315 + end
  316 +
  317 + def members_whitelist=(members)
  318 + settings[:members_whitelist] = members.split(',').map(&:to_i)
  319 + end
  320 +
306 321 def news_amount_by_folder=(amount)
307 322 settings[:news_amount_by_folder] = amount.to_i
308 323 end
... ... @@ -646,10 +661,11 @@ class Environment &lt; ActiveRecord::Base
646 661 { :controller => 'admin_panel', :action => 'index' }
647 662 end
648 663  
649   - def top_url
650   - url = 'http://'
  664 + def top_url(scheme = 'http')
  665 + url = scheme + '://'
651 666 url << (Noosfero.url_options.key?(:host) ? Noosfero.url_options[:host] : default_hostname)
652 667 url << ':' << Noosfero.url_options[:port].to_s if Noosfero.url_options.key?(:port)
  668 + url << Noosfero.root('')
653 669 url
654 670 end
655 671  
... ... @@ -717,31 +733,50 @@ class Environment &lt; ActiveRecord::Base
717 733 ]
718 734 end
719 735  
720   - def community_template
  736 + def is_default_template?(template)
  737 + is_default = template == community_default_template
  738 + is_default = is_default || template == person_default_template
  739 + is_default = is_default || template == enterprise_default_template
  740 + is_default
  741 + end
  742 +
  743 + def community_templates
  744 + self.communities.templates
  745 + end
  746 +
  747 + def community_default_template
721 748 template = Community.find_by_id settings[:community_template_id]
722   - template if template && template.is_template
  749 + template if template && template.is_template?
723 750 end
724 751  
725   - def community_template=(value)
726   - settings[:community_template_id] = value.id
  752 + def community_default_template=(value)
  753 + settings[:community_template_id] = value.kind_of?(Community) ? value.id : value
727 754 end
728 755  
729   - def person_template
  756 + def person_templates
  757 + self.people.templates
  758 + end
  759 +
  760 + def person_default_template
730 761 template = Person.find_by_id settings[:person_template_id]
731   - template if template && template.is_template
  762 + template if template && template.is_template?
  763 + end
  764 +
  765 + def person_default_template=(value)
  766 + settings[:person_template_id] = value.kind_of?(Person) ? value.id : value
732 767 end
733 768  
734   - def person_template=(value)
735   - settings[:person_template_id] = value.id
  769 + def enterprise_templates
  770 + self.enterprises.templates
736 771 end
737 772  
738   - def enterprise_template
  773 + def enterprise_default_template
739 774 template = Enterprise.find_by_id settings[:enterprise_template_id]
740   - template if template && template.is_template
  775 + template if template && template.is_template?
741 776 end
742 777  
743   - def enterprise_template=(value)
744   - settings[:enterprise_template_id] = value.id
  778 + def enterprise_default_template=(value)
  779 + settings[:enterprise_template_id] = value.kind_of?(Enterprise) ? value.id : value
745 780 end
746 781  
747 782 def inactive_enterprise_template
... ... @@ -793,8 +828,12 @@ class Environment &lt; ActiveRecord::Base
793 828 "home-page-news/#{cache_key}-#{language}"
794 829 end
795 830  
  831 + def portal_enabled
  832 + portal_community && enabled?('use_portal_community')
  833 + end
  834 +
796 835 def notification_emails
797   - [noreply_email.blank? ? nil : noreply_email].compact + admins.map(&:email)
  836 + [contact_email].select(&:present?) + admins.map(&:email)
798 837 end
799 838  
800 839 after_create :create_templates
... ... @@ -839,10 +878,10 @@ class Environment &lt; ActiveRecord::Base
839 878 person_template.visible = false
840 879 person_template.save!
841 880  
842   - self.enterprise_template = enterprise_template
  881 + self.enterprise_default_template = enterprise_template
843 882 self.inactive_enterprise_template = inactive_enterprise_template
844   - self.community_template = community_template
845   - self.person_template = person_template
  883 + self.community_default_template = community_template
  884 + self.person_default_template = person_template
846 885 self.save!
847 886 end
848 887  
... ... @@ -906,6 +945,10 @@ class Environment &lt; ActiveRecord::Base
906 945 locales_list
907 946 end
908 947  
  948 + def has_license?
  949 + self.licenses.any?
  950 + end
  951 +
909 952 private
910 953  
911 954 def default_language_available
... ...
app/models/environment_statistics_block.rb
... ... @@ -1,33 +0,0 @@
1   -# TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
2   -# the Noosfero core soon, see ActionItem3045
3   -
4   -class EnvironmentStatisticsBlock < Block
5   -
6   - def self.description
7   - _('Environment stastistics (DEPRECATED)')
8   - end
9   -
10   - def default_title
11   - _('Statistics for %s') % owner.name
12   - end
13   -
14   - def help
15   - _('This block presents some statistics about your environment.')
16   - end
17   -
18   - def content(args={})
19   - users = owner.people.visible.count
20   - enterprises = owner.enterprises.visible.count
21   - communities = owner.communities.visible.count
22   -
23   - info = []
24   - info << (n_('One user', '%{num} users', users) % { :num => users })
25   - unless owner.enabled?('disable_asset_enterprises')
26   - info << (n_('One enterprise', '%{num} enterprises', enterprises) % { :num => enterprises })
27   - end
28   - info << (n_('One community', '%{num} communities', communities) % { :num => communities })
29   -
30   - block_title(title) + content_tag('ul', info.map {|item| content_tag('li', item) }.join("\n"))
31   - end
32   -
33   -end
app/models/event.rb
... ... @@ -19,7 +19,7 @@ class Event &lt; Article
19 19 maybe_add_http(self.setting[:link])
20 20 end
21 21  
22   - xss_terminate :only => [ :body, :link, :address ], :with => 'white_list', :on => 'validation'
  22 + xss_terminate :only => [ :name, :body, :link, :address ], :with => 'white_list', :on => 'validation'
23 23  
24 24 def initialize(*args)
25 25 super(*args)
... ...
app/models/external_feed.rb
... ... @@ -10,9 +10,10 @@ class ExternalFeed &lt; ActiveRecord::Base
10 10 { :conditions => ['(fetched_at is NULL) OR (fetched_at < ?)', Time.now - FeedUpdater.update_interval] }
11 11 }
12 12  
13   - attr_accessible :address, :enabled
  13 + attr_accessible :address, :enabled, :only_once
14 14  
15 15 def add_item(title, link, date, content)
  16 + return if content.blank?
16 17 doc = Hpricot(content)
17 18 doc.search('*').each do |p|
18 19 if p.instance_of? Hpricot::Elem
... ... @@ -30,6 +31,7 @@ class ExternalFeed &lt; ActiveRecord::Base
30 31 article.source = link
31 32 article.profile = blog.profile
32 33 article.parent = blog
  34 + article.author_name = self.feed_title
33 35 unless blog.children.exists?(:slug => article.slug)
34 36 article.save!
35 37 article.delay.create_activity
... ...
app/models/feed_reader_block.rb
... ... @@ -85,8 +85,4 @@ class FeedReaderBlock &lt; Block
85 85 block_title(title) + formatted_feed_content
86 86 end
87 87  
88   - def editable?
89   - true
90   - end
91   -
92 88 end
... ...
app/models/folder.rb
... ... @@ -12,7 +12,7 @@ class Folder &lt; Article
12 12  
13 13 acts_as_having_settings :field => :setting
14 14  
15   - xss_terminate :only => [ :body ], :with => 'white_list', :on => 'validation'
  15 + xss_terminate :only => [ :name, :body ], :with => 'white_list', :on => 'validation'
16 16  
17 17 include WhiteListFilter
18 18 filter_iframes :body
... ...
app/models/invitation.rb
... ... @@ -51,7 +51,10 @@ class Invitation &lt; Task
51 51 next if contact_to_invite == _("Firstname Lastname <friend@email.com>")
52 52  
53 53 contact_to_invite.strip!
54   - if match = contact_to_invite.match(/(.*)<(.*)>/) and match[2].match(Noosfero::Constants::EMAIL_FORMAT)
  54 + find_by_profile_id = false
  55 + if contact_to_invite.match(/^\d*$/)
  56 + find_by_profile_id = true
  57 + elsif match = contact_to_invite.match(/(.*)<(.*)>/) and match[2].match(Noosfero::Constants::EMAIL_FORMAT)
55 58 friend_name = match[1].strip
56 59 friend_email = match[2]
57 60 elsif match = contact_to_invite.strip.match(Noosfero::Constants::EMAIL_FORMAT)
... ... @@ -61,22 +64,24 @@ class Invitation &lt; Task
61 64 next
62 65 end
63 66  
64   - user = User.find_by_email(friend_email)
  67 + begin
  68 + user = find_by_profile_id ? Person.find_by_id(contact_to_invite).user : User.find_by_email(friend_email)
  69 + rescue
  70 + user = nil
  71 + end
65 72  
66   - task_args = if user.nil?
  73 + task_args = if user.nil? && !find_by_profile_id
67 74 {:person => person, :friend_name => friend_name, :friend_email => friend_email, :message => message}
68   - elsif !user.person.is_a_friend?(person)
  75 + elsif user.present? && !(user.person.is_a_friend?(person) && profile.person?)
69 76 {:person => person, :target => user.person}
70 77 end
71 78  
72   - if !task_args.nil?
73   - if profile.person?
74   - InviteFriend.create(task_args)
75   - elsif profile.community?
76   - InviteMember.create(task_args.merge(:community_id => profile.id))
77   - else
78   - raise NotImplementedError, 'Don\'t know how to invite people to a %s' % profile.class.to_s
79   - end
  79 + if profile.person?
  80 + InviteFriend.create(task_args) if user.nil? || !user.person.is_a_friend?(person)
  81 + elsif profile.community?
  82 + InviteMember.create(task_args.merge(:community_id => profile.id)) if user.nil? || !user.person.is_member_of?(profile)
  83 + else
  84 + raise NotImplementedError, 'Don\'t know how to invite people to a %s' % profile.class.to_s
80 85 end
81 86 end
82 87 end
... ...
app/models/invite_friend.rb
1 1 class InviteFriend < Invitation
2 2  
3 3 settings_items :group_for_person, :group_for_friend
  4 + before_create :check_for_invitation_existence
4 5  
5 6 def perform
6 7 person.add_friend(friend, group_for_person)
... ... @@ -41,4 +42,11 @@ class InviteFriend &lt; Invitation
41 42 ].join("\n\n")
42 43 end
43 44  
  45 + private
  46 + def check_for_invitation_existence
  47 + if friend
  48 + friend.tasks.pending.of("InviteFriend").find(:all, :conditions => {:requestor_id => person.id, :target_id => friend.id}).blank?
  49 + end
  50 + end
  51 +
44 52 end
... ...
app/models/invite_member.rb
... ... @@ -2,6 +2,7 @@ class InviteMember &lt; Invitation
2 2  
3 3 settings_items :community_id, :type => :integer
4 4 validates_presence_of :community_id
  5 + before_create :check_for_invitation_existence
5 6  
6 7 def community
7 8 Community.find(community_id)
... ... @@ -39,6 +40,14 @@ class InviteMember &lt; Invitation
39 40 _('%{requestor} invited you to join %{community}.') % {:requestor => requestor.name, :community => community.name}
40 41 end
41 42  
  43 + def target_notification_message
  44 + if friend
  45 + _('%{requestor} is inviting you to join "%{community}" on %{system}.') % { :system => target.environment.name, :requestor => requestor.name, :community => community.name }
  46 + else
  47 + super
  48 + end
  49 + end
  50 +
42 51 def expanded_message
43 52 super.gsub /<community>/, community.name
44 53 end
... ... @@ -53,4 +62,11 @@ class InviteMember &lt; Invitation
53 62 ].join("\n\n")
54 63 end
55 64  
  65 + private
  66 + def check_for_invitation_existence
  67 + if friend
  68 + friend.tasks.pending.of("InviteMember").find(:all, :conditions => {:requestor_id => person.id}).select { |t| t.data[:community_id] == community_id }.blank?
  69 + end
  70 + end
  71 +
56 72 end
... ...
app/models/license.rb
... ... @@ -3,8 +3,8 @@ class License &lt; ActiveRecord::Base
3 3 attr_accessible :name, :url
4 4  
5 5 SEARCHABLE_FIELDS = {
6   - :name => 10,
7   - :url => 5,
  6 + :name => {:label => _('Name'), :weight => 10},
  7 + :url => {:label => _('URL'), :weight => 5},
8 8 }
9 9  
10 10 belongs_to :environment
... ...
app/models/link_article.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +class LinkArticle < Article
  2 +
  3 + attr_accessible :reference_article
  4 +
  5 + def self.short_description
  6 + "Article link"
  7 + end
  8 +
  9 + delegate :name, :to => :reference_article
  10 + delegate :body, :to => :reference_article
  11 + delegate :abstract, :to => :reference_article
  12 + delegate :url, :to => :reference_article
  13 +
  14 +end
... ...
app/models/link_list_block.rb
... ... @@ -78,16 +78,17 @@ class LinkListBlock &lt; Block
78 78 address
79 79 end
80 80 if add !~ /^[a-z]+:\/\// && add !~ /^\//
81   - 'http://' + add
  81 + '//' + add
82 82 else
  83 + if root = Noosfero.root
  84 + if !add.starts_with?(root)
  85 + add = root + add
  86 + end
  87 + end
83 88 add
84 89 end
85 90 end
86 91  
87   - def editable?
88   - true
89   - end
90   -
91 92 def icons_options
92 93 ICONS.map do |i|
93 94 "<span title=\"#{i[1]}\" class=\"icon-#{i[0]}\" onclick=\"changeIcon(this, '#{i[0]}')\"></span>".html_safe
... ... @@ -100,4 +101,5 @@ class LinkListBlock &lt; Block
100 101 sanitizer = HTML::WhiteListSanitizer.new
101 102 sanitizer.sanitize(text)
102 103 end
  104 +
103 105 end
... ...
app/models/main_block.rb
... ... @@ -16,10 +16,6 @@ class MainBlock &lt; Block
16 16 true
17 17 end
18 18  
19   - def editable?
20   - true
21   - end
22   -
23 19 def cacheable?
24 20 false
25 21 end
... ...
app/models/moderate_user_registration.rb 0 → 100644
... ... @@ -0,0 +1,59 @@
  1 +class ModerateUserRegistration < Task
  2 +
  3 + settings_items :user_id, :type => String
  4 + settings_items :name, :type => String
  5 + settings_items :author_name, :type => String
  6 + settings_items :email, :type => String
  7 +
  8 + after_create :schedule_spam_checking
  9 +
  10 + alias :environment :target
  11 + alias :environment= :target=
  12 +
  13 + def schedule_spam_checking
  14 + self.delay.check_for_spam
  15 + end
  16 +
  17 + include Noosfero::Plugin::HotSpot
  18 +
  19 + def sender
  20 + "#{name} (#{email})"
  21 + end
  22 +
  23 + def perform
  24 + user=environment.users.find_by_id(user_id)
  25 + user.activate
  26 + end
  27 +
  28 + def title
  29 + _("New user")
  30 + end
  31 +
  32 + def subject
  33 + name
  34 + end
  35 +
  36 + def information
  37 + { :message => _('%{sender} wants to register.'),
  38 + :variables => {:sender => sender} }
  39 + end
  40 +
  41 + def icon
  42 + result = {:type => :defined_image, :src => '/images/icons-app/person-minor.png', :name => name}
  43 + end
  44 +
  45 + def target_notification_description
  46 + _('%{sender} tried to register.') %
  47 + {:sender => sender}
  48 + end
  49 +
  50 + def target_notification_message
  51 + target_notification_description + "\n\n" +
  52 + _('You need to login on %{system} in order to approve or reject this user.') % { :environment => self.environment }
  53 + end
  54 +
  55 + def target_notification_message
  56 + _("User \"%{user}\" just requested to register. You have to approve or reject it through the \"Pending Validations\" section in your control panel.\n") % { :user => self.name }
  57 + end
  58 +
  59 +end
0 60 \ No newline at end of file
... ...
app/models/national_region.rb
1 1 class NationalRegion < ActiveRecord::Base
2 2  
3 3 SEARCHABLE_FIELDS = {
4   - :name => 1,
5   - :national_region_code => 1,
  4 + :name => {:label => _('Name'), :weight => 1},
  5 + :national_region_code => {:label => _('Region Code'), :weight => 1},
6 6 }
7 7  
8 8 def self.search_city(city_name, like = false, state = nil)
... ...
app/models/organization.rb
... ... @@ -3,10 +3,11 @@ class Organization &lt; Profile
3 3  
4 4 attr_accessible :moderated_articles, :foundation_year, :contact_person, :acronym, :legal_form, :economic_activity, :management_information, :cnpj, :display_name, :enable_contact_us
5 5  
6   - SEARCH_FILTERS += %w[
7   - more_popular
8   - more_active
9   - ]
  6 + SEARCH_FILTERS = {
  7 + :order => %w[more_recent more_popular more_active],
  8 + :display => %w[compact]
  9 + }
  10 +
10 11  
11 12 settings_items :closed, :type => :boolean, :default => false
12 13 def closed?
... ... @@ -30,6 +31,16 @@ class Organization &lt; Profile
30 31  
31 32 scope :more_popular, :order => 'members_count DESC'
32 33  
  34 + validate :presence_of_required_fieds, :unless => :is_template
  35 +
  36 + def presence_of_required_fieds
  37 + self.required_fields.each do |field|
  38 + if self.send(field).blank?
  39 + self.errors.add_on_blank(field)
  40 + end
  41 + end
  42 + end
  43 +
33 44 def validation_methodology
34 45 self.validation_info ? self.validation_info.validation_methodology : nil
35 46 end
... ... @@ -135,7 +146,11 @@ class Organization &lt; Profile
135 146 end
136 147  
137 148 def notification_emails
138   - [contact_email.blank? ? nil : contact_email].compact + admins.map(&:email)
  149 + emails = [contact_email].select(&:present?) + admins.map(&:email)
  150 + if emails.empty?
  151 + emails << environment.contact_email
  152 + end
  153 + emails
139 154 end
140 155  
141 156 def already_request_membership?(person)
... ... @@ -162,4 +177,8 @@ class Organization &lt; Profile
162 177 self.visible = false
163 178 save!
164 179 end
  180 +
  181 + def allow_invitation_from?(person)
  182 + (followed_by?(person) && self.allow_members_to_invite) || person.has_permission?('invite-members', self)
  183 + end
165 184 end
... ...
app/models/person.rb
... ... @@ -3,10 +3,11 @@ class Person &lt; Profile
3 3  
4 4 attr_accessible :organization, :contact_information, :sex, :birth_date, :cell_phone, :comercial_phone, :jabber_id, :personal_website, :nationality, :address_reference, :district, :schooling, :schooling_status, :formation, :custom_formation, :area_of_study, :custom_area_of_study, :professional_activity, :organization_website
5 5  
6   - SEARCH_FILTERS += %w[
7   - more_popular
8   - more_active
9   - ]
  6 + SEARCH_FILTERS = {
  7 + :order => %w[more_recent more_popular more_active],
  8 + :display => %w[compact]
  9 + }
  10 +
10 11  
11 12 def self.type_name
12 13 _('Person')
... ... @@ -21,10 +22,34 @@ class Person &lt; Profile
21 22 { :select => 'DISTINCT profiles.*', :joins => :role_assignments, :conditions => [conditions] }
22 23 }
23 24  
24   - def has_permission_with_plugins?(permission, profile)
25   - permissions = [has_permission_without_plugins?(permission, profile)]
  25 + scope :not_members_of, lambda { |resources|
  26 + resources = [resources] if !resources.kind_of?(Array)
  27 + conditions = resources.map {|resource| "role_assignments.resource_type = '#{resource.class.base_class.name}' AND role_assignments.resource_id = #{resource.id || -1}"}.join(' OR ')
  28 + { :select => 'DISTINCT profiles.*', :conditions => ['"profiles"."id" NOT IN (SELECT DISTINCT profiles.id FROM "profiles" INNER JOIN "role_assignments" ON "role_assignments"."accessor_id" = "profiles"."id" AND "role_assignments"."accessor_type" = (\'Profile\') WHERE "profiles"."type" IN (\'Person\') AND (%s))' % conditions] }
  29 + }
  30 +
  31 + scope :by_role, lambda { |roles|
  32 + roles = [roles] unless roles.kind_of?(Array)
  33 + { :select => 'DISTINCT profiles.*', :joins => :role_assignments, :conditions => ['role_assignments.role_id IN (?)',
  34 +roles] }
  35 + }
  36 +
  37 + scope :not_friends_of, lambda { |resources|
  38 + resources = Array(resources)
  39 + { :select => 'DISTINCT profiles.*', :conditions => ['"profiles"."id" NOT IN (SELECT DISTINCT profiles.id FROM "profiles" INNER JOIN "friendships" ON "friendships"."person_id" = "profiles"."id" WHERE "friendships"."friend_id" IN (%s))' % resources.map(&:id)] }
  40 + }
  41 +
  42 + def has_permission_with_admin?(permission, resource)
  43 + return true if resource.blank? || resource.admins.include?(self)
  44 + return true if resource.kind_of?(Profile) && resource.environment.admins.include?(self)
  45 + has_permission_without_admin?(permission, resource)
  46 + end
  47 + alias_method_chain :has_permission?, :admin
  48 +
  49 + def has_permission_with_plugins?(permission, resource)
  50 + permissions = [has_permission_without_plugins?(permission, resource)]
26 51 permissions += plugins.map do |plugin|
27   - plugin.has_permission?(self, permission, profile)
  52 + plugin.has_permission?(self, permission, resource)
28 53 end
29 54 permissions.include?(true)
30 55 end
... ... @@ -39,9 +64,9 @@ class Person &lt; Profile
39 64 ScopeTool.union *scopes
40 65 end
41 66  
42   - def memberships_by_role(role)
43   - memberships.where('role_assignments.role_id = ?', role.id)
44   - end
  67 + def memberships_by_role(role)
  68 + memberships.where('role_assignments.role_id = ?', role.id)
  69 + end
45 70  
46 71 has_many :friendships, :dependent => :destroy
47 72 has_many :friends, :class_name => 'Person', :through => :friendships
... ... @@ -59,6 +84,10 @@ class Person &lt; Profile
59 84 has_and_belongs_to_many :acepted_forums, :class_name => 'Forum', :join_table => 'terms_forum_people'
60 85 has_and_belongs_to_many :articles_with_access, :class_name => 'Article', :join_table => 'article_privacy_exceptions'
61 86  
  87 + has_many :profile_suggestions, :foreign_key => :person_id, :order => 'score DESC', :dependent => :destroy
  88 + has_many :suggested_people, :through => :profile_suggestions, :source => :suggestion, :conditions => ['profile_suggestions.suggestion_type = ? AND profile_suggestions.enabled = ?', 'Person', true]
  89 + has_many :suggested_communities, :through => :profile_suggestions, :source => :suggestion, :conditions => ['profile_suggestions.suggestion_type = ? AND profile_suggestions.enabled = ?', 'Community', true]
  90 +
62 91 scope :more_popular, :order => 'friends_count DESC'
63 92  
64 93 scope :abusers, :joins => :abuse_complaints, :conditions => ['tasks.status = 3'], :select => 'DISTINCT profiles.*'
... ... @@ -74,6 +103,10 @@ class Person &lt; Profile
74 103  
75 104 belongs_to :user, :dependent => :delete
76 105  
  106 + def can_change_homepage?
  107 + !environment.enabled?('cant_change_homepage') || is_admin?
  108 + end
  109 +
77 110 def can_control_scrap?(scrap)
78 111 begin
79 112 !self.scraps(scrap).nil?
... ... @@ -108,12 +141,12 @@ class Person &lt; Profile
108 141 end
109 142  
110 143 def add_friend(friend, group = nil)
111   - unless self.is_a_friend?(friend)
  144 + unless self.is_a_friend?(friend)
112 145 friendship = self.friendships.build
113 146 friendship.friend = friend
114 147 friendship.group = group
115 148 friendship.save
116   - end
  149 + end
117 150 end
118 151  
119 152 def already_request_friendship?(person)
... ... @@ -161,7 +194,7 @@ class Person &lt; Profile
161 194 FIELDS
162 195 end
163 196  
164   - validate :presence_of_required_fields
  197 + validate :presence_of_required_fields, :unless => :is_template
165 198  
166 199 def presence_of_required_fields
167 200 self.required_fields.each do |field|
... ... @@ -300,7 +333,7 @@ class Person &lt; Profile
300 333 end
301 334  
302 335 def default_template
303   - environment.person_template
  336 + environment.person_default_template
304 337 end
305 338  
306 339 def apply_type_specific_template(template)
... ... @@ -487,6 +520,15 @@ class Person &lt; Profile
487 520 person.notifier.reschedule_next_notification_mail
488 521 end
489 522  
  523 + def remove_suggestion(profile)
  524 + suggestion = profile_suggestions.find_by_suggestion_id profile.id
  525 + suggestion.disable if suggestion
  526 + end
  527 +
  528 + def allow_invitation_from?(person)
  529 + person.has_permission?(:manage_friends, self)
  530 + end
  531 +
490 532 protected
491 533  
492 534 def followed_by?(profile)
... ...
app/models/person_notifier.rb
... ... @@ -67,7 +67,7 @@ class PersonNotifier
67 67  
68 68 class Mailer < ActionMailer::Base
69 69  
70   - add_template_helper(PersonNotifierHelper)
  70 + add_template_helper(ApplicationHelper)
71 71  
72 72 def session
73 73 {:theme => nil}
... ...
app/models/product.rb
1 1 class Product < ActiveRecord::Base
2 2  
3 3 SEARCHABLE_FIELDS = {
4   - :name => 10,
5   - :description => 1,
  4 + :name => {:label => _('Name'), :weight => 10},
  5 + :description => {:label => _('Description'), :weight => 1},
6 6 }
7 7  
8   - SEARCH_FILTERS = %w[
9   - more_recent
10   - ]
11   -
12   - SEARCH_DISPLAYS = %w[map full]
  8 + SEARCH_FILTERS = {
  9 + :order => %w[more_recent],
  10 + :display => %w[full map]
  11 + }
13 12  
14   - attr_accessible :name, :product_category, :highlighted, :price, :enterprise, :image_builder, :description, :available, :qualifiers, :unit_id, :discount, :inputs
  13 + attr_accessible :name, :product_category, :highlighted, :price, :enterprise, :image_builder, :description, :available, :qualifiers, :unit_id, :discount, :inputs, :qualifiers_list
15 14  
16 15 def self.default_search_display
17 16 'full'
... ...
app/models/profile.rb
... ... @@ -3,7 +3,7 @@
3 3 # which by default is the one returned by Environment:default.
4 4 class Profile < ActiveRecord::Base
5 5  
6   - attr_accessible :name, :identifier, :public_profile, :nickname, :custom_footer, :custom_header, :address, :zip_code, :contact_phone, :image_builder, :description, :closed, :template_id, :environment, :lat, :lng, :is_template, :fields_privacy, :preferred_domain_id, :category_ids, :country, :city, :state, :national_region_code, :email, :contact_email, :redirect_l10n, :notification_time, :redirection_after_login
  6 + attr_accessible :name, :identifier, :public_profile, :nickname, :custom_footer, :custom_header, :address, :zip_code, :contact_phone, :image_builder, :description, :closed, :template_id, :environment, :lat, :lng, :is_template, :fields_privacy, :preferred_domain_id, :category_ids, :country, :city, :state, :national_region_code, :email, :contact_email, :redirect_l10n, :notification_time, :redirection_after_login, :email_suggestions, :allow_members_to_invite, :invite_friends_only
7 7  
8 8 # use for internationalizable human type names in search facets
9 9 # reimplement on subclasses
... ... @@ -12,16 +12,15 @@ class Profile &lt; ActiveRecord::Base
12 12 end
13 13  
14 14 SEARCHABLE_FIELDS = {
15   - :name => 10,
16   - :identifier => 5,
17   - :nickname => 2,
  15 + :name => {:label => _('Name'), :weight => 10},
  16 + :identifier => {:label => _('Username'), :weight => 5},
  17 + :nickname => {:label => _('Nickname'), :weight => 2},
18 18 }
19 19  
20   - SEARCH_FILTERS = %w[
21   - more_recent
22   - ]
23   -
24   - SEARCH_DISPLAYS = %w[compact]
  20 + SEARCH_FILTERS = {
  21 + :order => %w[more_recent],
  22 + :display => %w[compact]
  23 + }
25 24  
26 25 def self.default_search_display
27 26 'compact'
... ... @@ -97,7 +96,7 @@ class Profile &lt; ActiveRecord::Base
97 96 end
98 97  
99 98 def members_by_name
100   - members.order(:name)
  99 + members.order('profiles.name')
101 100 end
102 101  
103 102 class << self
... ... @@ -108,8 +107,8 @@ class Profile &lt; ActiveRecord::Base
108 107 alias_method_chain :count, :distinct
109 108 end
110 109  
111   - def members_by_role(role)
112   - Person.members_of(self).all(:conditions => ['role_assignments.role_id = ?', role.id])
  110 + def members_by_role(roles)
  111 + Person.members_of(self).by_role(roles)
113 112 end
114 113  
115 114 acts_as_having_boxes
... ... @@ -121,7 +120,9 @@ class Profile &lt; ActiveRecord::Base
121 120 end
122 121  
123 122 scope :visible, :conditions => { :visible => true }
  123 + scope :disabled, :conditions => { :visible => false }
124 124 scope :public, :conditions => { :visible => true, :public_profile => true }
  125 + scope :enabled, :conditions => { :enabled => true }
125 126  
126 127 # Subclasses must override this method
127 128 scope :more_popular
... ... @@ -138,6 +139,17 @@ class Profile &lt; ActiveRecord::Base
138 139  
139 140 has_many :comments_received, :class_name => 'Comment', :through => :articles, :source => :comments
140 141  
  142 + # Although this should be a has_one relation, there are no non-silly names for
  143 + # a foreign key on article to reference the template to which it is
  144 + # welcome_page... =P
  145 + belongs_to :welcome_page, :class_name => 'Article', :dependent => :destroy
  146 +
  147 + def welcome_page_content
  148 + welcome_page && welcome_page.published ? welcome_page.body : nil
  149 + end
  150 +
  151 + has_many :search_terms, :as => :context
  152 +
141 153 def scraps(scrap=nil)
142 154 scrap = scrap.is_a?(Scrap) ? scrap.id : scrap
143 155 scrap.nil? ? Scrap.all_scraps(self) : Scrap.all_scraps(self).find(scrap)
... ... @@ -153,6 +165,7 @@ class Profile &lt; ActiveRecord::Base
153 165 settings_items :public_content, :type => :boolean, :default => true
154 166 settings_items :description
155 167 settings_items :fields_privacy, :type => :hash, :default => {}
  168 + settings_items :email_suggestions, :type => :boolean, :default => false
156 169  
157 170 validates_length_of :description, :maximum => 550, :allow_nil => true
158 171  
... ... @@ -217,6 +230,8 @@ class Profile &lt; ActiveRecord::Base
217 230  
218 231 has_many :abuse_complaints, :foreign_key => 'requestor_id', :dependent => :destroy
219 232  
  233 + has_many :profile_suggestions, :foreign_key => :suggestion_id, :dependent => :destroy
  234 +
220 235 def top_level_categorization
221 236 ret = {}
222 237 self.profile_categorizations.each do |c|
... ... @@ -346,16 +361,17 @@ class Profile &lt; ActiveRecord::Base
346 361 end
347 362  
348 363 def copy_blocks_from(profile)
  364 + template_boxes = profile.boxes.select{|box| box.position}
349 365 self.boxes.destroy_all
350   - profile.boxes.each do |box|
351   - new_box = Box.new
  366 + self.boxes = template_boxes.size.times.map { Box.new }
  367 +
  368 + template_boxes.each_with_index do |box, i|
  369 + new_box = self.boxes[i]
352 370 new_box.position = box.position
353   - self.boxes << new_box
354 371 box.blocks.each do |block|
355 372 new_block = block.class.new(:title => block[:title])
356   - new_block.settings = block.settings
357   - new_block.position = block.position
358   - self.boxes[-1].blocks << new_block
  373 + new_block.copy_from(block)
  374 + new_box.blocks << new_block
359 375 end
360 376 end
361 377 end
... ... @@ -390,7 +406,7 @@ class Profile &lt; ActiveRecord::Base
390 406 end
391 407  
392 408 xss_terminate :only => [ :name, :nickname, :address, :contact_phone, :description ], :on => 'validation'
393   - xss_terminate :only => [ :custom_footer, :custom_header ], :with => 'white_list', :on => 'validation'
  409 + xss_terminate :only => [ :custom_footer, :custom_header ], :with => 'white_list'
394 410  
395 411 include WhiteListFilter
396 412 filter_iframes :custom_header, :custom_footer
... ... @@ -511,6 +527,14 @@ class Profile &lt; ActiveRecord::Base
511 527 generate_url(:profile => identifier, :controller => 'profile', :action => 'index')
512 528 end
513 529  
  530 + def people_suggestions_url
  531 + generate_url(:profile => identifier, :controller => 'friends', :action => 'suggest')
  532 + end
  533 +
  534 + def communities_suggestions_url
  535 + generate_url(:profile => identifier, :controller => 'memberships', :action => 'suggest')
  536 + end
  537 +
514 538 def generate_url(options)
515 539 url_options.merge(options)
516 540 end
... ... @@ -600,7 +624,7 @@ private :generate_url, :url_options
600 624 end
601 625  
602 626 def copy_article_tree(article, parent=nil)
603   - return if article.is_a?(RssFeed)
  627 + return if !copy_article?(article)
604 628 original_article = self.articles.find_by_name(article.name)
605 629 if original_article
606 630 num = 2
... ... @@ -620,6 +644,11 @@ private :generate_url, :url_options
620 644 end
621 645 end
622 646  
  647 + def copy_article?(article)
  648 + !article.is_a?(RssFeed) &&
  649 + !(is_template && article.slug=='welcome-page')
  650 + end
  651 +
623 652 # Adds a person as member of this Profile.
624 653 def add_member(person)
625 654 if self.has_members?
... ... @@ -629,6 +658,8 @@ private :generate_url, :url_options
629 658 self.affiliate(person, Profile::Roles.admin(environment.id)) if members.count == 0
630 659 self.affiliate(person, Profile::Roles.member(environment.id))
631 660 end
  661 + person.tasks.pending.of("InviteMember").select { |t| t.data[:community_id] == self.id }.each { |invite| invite.cancel }
  662 + remove_from_suggestion_list person
632 663 else
633 664 raise _("%s can't have members") % self.class.name
634 665 end
... ... @@ -766,7 +797,10 @@ private :generate_url, :url_options
766 797 end
767 798  
768 799 def admins
769   - self.members_by_role(Profile::Roles.admin(environment.id))
  800 + return [] if environment.blank?
  801 + admin_role = Profile::Roles.admin(environment.id)
  802 + return [] if admin_role.blank?
  803 + self.members_by_role(admin_role)
770 804 end
771 805  
772 806 def enable_contact?
... ... @@ -774,7 +808,7 @@ private :generate_url, :url_options
774 808 end
775 809  
776 810 include Noosfero::Plugin::HotSpot
777   -
  811 +
778 812 def folder_types
779 813 types = Article.folder_types
780 814 plugins.dispatch(:content_types).each {|type|
... ... @@ -898,6 +932,13 @@ private :generate_url, :url_options
898 932 end
899 933  
900 934 def disable
  935 + self.visible = false
  936 + self.save
  937 + end
  938 +
  939 + def enable
  940 + self.visible = true
  941 + self.save
901 942 end
902 943  
903 944 def control_panel_settings_button
... ... @@ -957,4 +998,14 @@ private :generate_url, :url_options
957 998 def preferred_login_redirection
958 999 redirection_after_login.blank? ? environment.redirection_after_login : redirection_after_login
959 1000 end
  1001 +
  1002 + def remove_from_suggestion_list(person)
  1003 + suggestion = person.profile_suggestions.find_by_suggestion_id self.id
  1004 + suggestion.disable if suggestion
  1005 + end
  1006 +
  1007 + def allow_invitation_from(person)
  1008 + false
  1009 + end
  1010 +
960 1011 end
... ...
app/models/profile_image_block.rb
... ... @@ -23,10 +23,6 @@ class ProfileImageBlock &lt; Block
23 23 end
24 24 end
25 25  
26   - def editable?
27   - true
28   - end
29   -
30 26 def cacheable?
31 27 false
32 28 end
... ...
app/models/profile_info_block.rb
... ... @@ -15,10 +15,6 @@ class ProfileInfoBlock &lt; Block
15 15 end
16 16 end
17 17  
18   - def editable?
19   - false
20   - end
21   -
22 18 def cacheable?
23 19 false
24 20 end
... ...
app/models/profile_search_block.rb
... ... @@ -11,8 +11,4 @@ class ProfileSearchBlock &lt; Block
11 11 end
12 12 end
13 13  
14   - def editable?
15   - true
16   - end
17   -
18 14 end
... ...
app/models/profile_suggestion.rb 0 → 100644
... ... @@ -0,0 +1,290 @@
  1 +class ProfileSuggestion < ActiveRecord::Base
  2 + belongs_to :person
  3 + belongs_to :suggestion, :class_name => 'Profile', :foreign_key => :suggestion_id
  4 +
  5 + attr_accessible :person, :suggestion, :suggestion_type, :categories, :enabled
  6 +
  7 + has_many :suggestion_connections, :foreign_key => 'suggestion_id'
  8 + has_many :profile_connections, :through => :suggestion_connections, :source => :connection, :source_type => 'Profile'
  9 + has_many :tag_connections, :through => :suggestion_connections, :source => :connection, :source_type => 'ActsAsTaggableOn::Tag'
  10 +
  11 + before_create do |profile_suggestion|
  12 + profile_suggestion.suggestion_type = self.suggestion.class.to_s
  13 + end
  14 +
  15 + after_destroy do |profile_suggestion|
  16 + self.class.generate_profile_suggestions(profile_suggestion.person)
  17 + end
  18 +
  19 + acts_as_having_settings :field => :categories
  20 +
  21 + validate :must_be_a_valid_category, :on => :create
  22 + def must_be_a_valid_category
  23 + if categories.keys.map { |cat| self.respond_to?(cat)}.include?(false)
  24 + errors.add(:categories, 'Category must be valid')
  25 + end
  26 + end
  27 +
  28 + validates_uniqueness_of :suggestion_id, :scope => [ :person_id ]
  29 + scope :of_person, :conditions => { :suggestion_type => 'Person' }
  30 + scope :of_community, :conditions => { :suggestion_type => 'Community' }
  31 + scope :enabled, :conditions => { :enabled => true }
  32 +
  33 + # {:category_type => ['category-icon', 'category-label']}
  34 + CATEGORIES = {
  35 + :people_with_common_friends => ['menu-people', _('Friends in common')],
  36 + :people_with_common_communities => ['menu-community',_('Communities in common')],
  37 + :people_with_common_tags => ['edit', _('Tags in common')],
  38 + :communities_with_common_friends => ['menu-people', _('Friends in common')],
  39 + :communities_with_common_tags => ['edit', _('Tags in common')]
  40 + }
  41 +
  42 + def category_icon(category)
  43 + 'icon-' + ProfileSuggestion::CATEGORIES[category][0]
  44 + end
  45 +
  46 + def category_label(category)
  47 + ProfileSuggestion::CATEGORIES[category][1]
  48 + end
  49 +
  50 + RULES = {
  51 + :people_with_common_communities => {
  52 + :threshold => 2, :weight => 1, :connection => 'Profile'
  53 + },
  54 + :people_with_common_friends => {
  55 + :threshold => 2, :weight => 1, :connection => 'Profile'
  56 + },
  57 + :people_with_common_tags => {
  58 + :threshold => 2, :weight => 1, :connection => 'ActsAsTaggableOn::Tag'
  59 + },
  60 + :communities_with_common_friends => {
  61 + :threshold => 2, :weight => 1, :connection => 'Profile'
  62 + },
  63 + :communities_with_common_tags => {
  64 + :threshold => 2, :weight => 1, :connection => 'ActsAsTaggableOn::Tag'
  65 + }
  66 + }
  67 +
  68 + RULES.keys.each do |rule|
  69 + settings_items rule
  70 + attr_accessible rule
  71 + end
  72 +
  73 + # Number of suggestions by rule
  74 + N_SUGGESTIONS = 30
  75 +
  76 + # Minimum number of suggestions
  77 + MIN_LIMIT = 10
  78 +
  79 + def self.profile_id(rule)
  80 + "#{rule}_profile_id"
  81 + end
  82 +
  83 + def self.connections(rule)
  84 + "#{rule}_connections"
  85 + end
  86 +
  87 + def self.counter(rule)
  88 + "#{rule}_count"
  89 + end
  90 +
  91 + # If you are about to rewrite the following sql queries, think twice. After
  92 + # that make sure that whatever you are writing to replace it should be faster
  93 + # than how it is now. Yes, sqls are ugly but are fast! And fast is what we
  94 + # need here.
  95 + #
  96 + # The logic behind this code is to produce a table somewhat like this:
  97 + # profile_id | rule1_count | rule1_connections | rule2_count | rule2_connections | ... | score |
  98 + # 12 | 2 | {32,54} | 3 | {8,22,27} | ... | 13 |
  99 + # 13 | 4 | {3,12,32,54} | 2 | {11,24} | ... | 15 |
  100 + # 14 | | | 2 | {44,56} | ... | 17 |
  101 + # ...
  102 + # ...
  103 + #
  104 + # This table has the suggested profile id and the count and connections of
  105 + # each rule that made this profile be suggested. Each suggestion has a score
  106 + # associated based on the rules' counts and rules' weights.
  107 + #
  108 + # From this table, we can sort suggestions by the score and save a small
  109 + # amount of them in the database. At this moment we also register the
  110 + # connections of each suggestion.
  111 +
  112 + def self.calculate_suggestions(person)
  113 + suggested_profiles = all_suggestions(person)
  114 + return if suggested_profiles.nil?
  115 +
  116 + already_suggested_profiles = person.profile_suggestions.map(&:suggestion_id).join(',')
  117 + suggested_profiles = suggested_profiles.where("profiles.id NOT IN (#{already_suggested_profiles})") if already_suggested_profiles.present?
  118 + #TODO suggested_profiles = suggested_profiles.order('score DESC')
  119 + suggested_profiles = suggested_profiles.limit(N_SUGGESTIONS)
  120 + return if suggested_profiles.blank?
  121 +
  122 + suggested_profiles.each do |suggested_profile|
  123 + suggestion = person.profile_suggestions.find_or_initialize_by_suggestion_id(suggested_profile.id)
  124 + RULES.each do |rule, options|
  125 + begin
  126 + value = suggested_profile.send("#{rule}_count").to_i
  127 + rescue NoMethodError
  128 + next
  129 + end
  130 + connections = suggested_profile.send("#{rule}_connections")
  131 + if connections.present?
  132 + connections = connections[1..-2].split(',')
  133 + else
  134 + connections = []
  135 + end
  136 + suggestion.send("#{rule}=", value)
  137 + connections.each do |connection_id|
  138 + next if SuggestionConnection.where(:suggestion_id => suggestion.id, :connection_id => connection_id, :connection_type => options[:connection]).present?
  139 + SuggestionConnection.create!(:suggestion => suggestion, :connection_id => connection_id, :connection_type => options[:connection])
  140 + end
  141 + suggestion.score += value * options[:weight]
  142 + end
  143 + suggestion.save!
  144 + end
  145 + end
  146 +
  147 + def self.people_with_common_friends(person)
  148 + person_friends = person.friends.map(&:id)
  149 + rule = "people_with_common_friends"
  150 + return if person_friends.blank?
  151 + "SELECT person_id as #{profile_id(rule)},
  152 + array_agg(friend_id) as #{connections(rule)},
  153 + count(person_id) as #{counter(rule)}
  154 + FROM friendships WHERE friend_id IN (#{person_friends.join(',')})
  155 + AND person_id NOT IN (#{(person_friends << person.id).join(',')})
  156 + GROUP BY person_id"
  157 + end
  158 +
  159 + def self.people_with_common_communities(person)
  160 + person_communities = person.communities.map(&:id)
  161 + rule = "people_with_common_communities"
  162 + return if person_communities.blank?
  163 + "SELECT common_members.accessor_id as #{profile_id(rule)},
  164 + array_agg(common_members.resource_id) as #{connections(rule)},
  165 + count(common_members.accessor_id) as #{counter(rule)}
  166 + FROM
  167 + (SELECT DISTINCT accessor_id, resource_id FROM
  168 + role_assignments WHERE role_assignments.resource_id IN (#{person_communities.join(',')}) AND
  169 + role_assignments.accessor_id != #{person.id} AND role_assignments.resource_type = 'Profile' AND
  170 + role_assignments.accessor_type = 'Profile') AS common_members
  171 + GROUP BY common_members.accessor_id"
  172 + end
  173 +
  174 + def self.people_with_common_tags(person)
  175 + profile_tags = person.articles.select('tags.id').joins(:tags).map(&:id)
  176 + rule = "people_with_common_tags"
  177 + return if profile_tags.blank?
  178 + "SELECT results.profiles_id as #{profile_id(rule)},
  179 + array_agg(results.tags_id) as #{connections(rule)},
  180 + count(results.profiles_id) as #{counter(rule)}
  181 + FROM (
  182 + SELECT DISTINCT tags.id as tags_id, profiles.id as profiles_id FROM profiles
  183 + INNER JOIN articles ON articles.profile_id = profiles.id
  184 + INNER JOIN taggings ON taggings.taggable_id = articles.id AND taggings.context = ('tags') AND taggings.taggable_type = 'Article'
  185 + INNER JOIN tags ON tags.id = taggings.tag_id
  186 + WHERE (tags.id in (#{profile_tags.join(',')}) AND profiles.id != #{person.id})) AS results
  187 + GROUP BY results.profiles_id"
  188 + end
  189 +
  190 + def self.communities_with_common_friends(person)
  191 + person_friends = person.friends.map(&:id)
  192 + rule = "communities_with_common_friends"
  193 + return if person_friends.blank?
  194 + "SELECT common_communities.resource_id as #{profile_id(rule)},
  195 + array_agg(common_communities.accessor_id) as #{connections(rule)},
  196 + count(common_communities.resource_id) as #{counter(rule)}
  197 + FROM
  198 + (SELECT DISTINCT accessor_id, resource_id FROM
  199 + role_assignments WHERE role_assignments.accessor_id IN (#{person_friends.join(',')}) AND
  200 + role_assignments.accessor_id != #{person.id} AND role_assignments.resource_type = 'Profile' AND
  201 + role_assignments.accessor_type = 'Profile') AS common_communities
  202 + GROUP BY common_communities.resource_id"
  203 + end
  204 +
  205 + def self.communities_with_common_tags(person)
  206 + profile_tags = person.articles.select('tags.id').joins(:tags).map(&:id)
  207 + rule = "communities_with_common_tags"
  208 + return if profile_tags.blank?
  209 + "SELECT results.profiles_id as #{profile_id(rule)},
  210 + array_agg(results.tags_id) as #{connections(rule)},
  211 + count(results.profiles_id) as #{counter(rule)}
  212 + FROM
  213 + (SELECT DISTINCT tags.id as tags_id, profiles.id AS profiles_id FROM profiles
  214 + INNER JOIN articles ON articles.profile_id = profiles.id
  215 + INNER JOIN taggings ON taggings.taggable_id = articles.id AND taggings.context = ('tags') AND taggings.taggable_type = 'Article'
  216 + INNER JOIN tags ON tags.id = taggings.tag_id
  217 + WHERE (tags.id IN (#{profile_tags.join(',')}) AND profiles.id != #{person.id})) AS results
  218 + GROUP BY results.profiles_id"
  219 + end
  220 +
  221 + def self.all_suggestions(person)
  222 + select_string = ["profiles.*"]
  223 + suggestions_join = []
  224 + where_string = []
  225 + valid_rules = []
  226 + previous_rule = nil
  227 + join_column = nil
  228 + RULES.each do |rule, options|
  229 + rule_select = self.send(rule, person)
  230 + next if !rule_select.present?
  231 +
  232 + valid_rules << rule
  233 + select_string << "suggestions.#{counter(rule)} as #{counter(rule)}, suggestions.#{connections(rule)} as #{connections(rule)}"
  234 + where_string << "#{counter(rule)} >= #{options[:threshold]}"
  235 + rule_select = "
  236 + (SELECT profiles.id as #{profile_id(rule)},
  237 + #{rule}_sub.#{counter(rule)} as #{counter(rule)},
  238 + #{rule}_sub.#{connections(rule)} as #{connections(rule)}
  239 + FROM profiles
  240 + LEFT OUTER JOIN (#{rule_select}) as #{rule}_sub
  241 + ON profiles.id = #{rule}_sub.#{profile_id(rule)}) AS #{rule}"
  242 +
  243 + if previous_rule.nil?
  244 + result = rule_select
  245 + else
  246 + result = "INNER JOIN #{rule_select}
  247 + ON #{previous_rule}.#{profile_id(previous_rule)} = #{rule}.#{profile_id(rule)}"
  248 + end
  249 + previous_rule = rule
  250 + suggestions_join << result
  251 + end
  252 +
  253 + return if valid_rules.blank?
  254 +
  255 + select_string = select_string.compact.join(',')
  256 + join_string = "INNER JOIN (SELECT * FROM #{suggestions_join.compact.join(' ')}) AS suggestions ON profiles.id = suggestions.#{profile_id(valid_rules.first)}"
  257 + where_string = where_string.compact.join(' OR ')
  258 +
  259 + person.environment.profiles.
  260 + select(select_string).
  261 + joins(join_string).
  262 + where(where_string)
  263 + end
  264 +
  265 + def disable
  266 + self.enabled = false
  267 + self.save!
  268 + self.class.generate_profile_suggestions(self.person)
  269 + end
  270 +
  271 + def self.generate_all_profile_suggestions
  272 + Delayed::Job.enqueue(ProfileSuggestion::GenerateAllJob.new) unless ProfileSuggestion::GenerateAllJob.exists?
  273 + end
  274 +
  275 + def self.generate_profile_suggestions(person, force = false)
  276 + return if person.profile_suggestions.enabled.count >= MIN_LIMIT && !force
  277 + Delayed::Job.enqueue ProfileSuggestionsJob.new(person.id) unless ProfileSuggestionsJob.exists?(person.id)
  278 + end
  279 +
  280 + class GenerateAllJob
  281 + def self.exists?
  282 + Delayed::Job.by_handler("--- !ruby/object:ProfileSuggestion::GenerateAllJob {}\n").count > 0
  283 + end
  284 +
  285 + def perform
  286 + Person.find_each {|person| ProfileSuggestion.generate_profile_suggestions(person) }
  287 + end
  288 + end
  289 +
  290 +end
... ...
app/models/qualifier.rb
... ... @@ -3,7 +3,7 @@ class Qualifier &lt; ActiveRecord::Base
3 3 attr_accessible :name, :environment
4 4  
5 5 SEARCHABLE_FIELDS = {
6   - :name => 1,
  6 + :name => {:label => _('Name'), :weight => 1},
7 7 }
8 8  
9 9 belongs_to :environment
... ...
app/models/scrap.rb
... ... @@ -3,7 +3,7 @@ class Scrap &lt; ActiveRecord::Base
3 3 attr_accessible :content, :sender_id, :receiver_id, :scrap_id
4 4  
5 5 SEARCHABLE_FIELDS = {
6   - :content => 1,
  6 + :content => {:label => _('Content'), :weight => 1},
7 7 }
8 8 validates_presence_of :content
9 9 validates_presence_of :sender_id, :receiver_id
... ...
app/models/search_term.rb 0 → 100644
... ... @@ -0,0 +1,63 @@
  1 +class SearchTerm < ActiveRecord::Base
  2 + validates_presence_of :term, :context
  3 + validates_uniqueness_of :term, :scope => [:context_id, :context_type, :asset]
  4 +
  5 + belongs_to :context, :polymorphic => true
  6 + has_many :occurrences, :class_name => 'SearchTermOccurrence'
  7 +
  8 + attr_accessible :term, :context, :asset
  9 +
  10 + def self.calculate_scores
  11 + os = occurrences_scores
  12 + find_each { |search_term| search_term.calculate_score(os) }
  13 + end
  14 +
  15 + def self.find_or_create(term, context, asset='all')
  16 + context.search_terms.where(:term => term, :asset => asset).first || context.search_terms.create!(:term => term, :asset=> asset)
  17 + end
  18 +
  19 + # Fast way of getting the occurrences score for each search_term. Ugly but fast!
  20 + #
  21 + # Each occurrence of a search_term has a score that is smaller the older the
  22 + # occurrence happened. We subtract the amount of time between now and the
  23 + # moment it happened from the total time any occurrence is valid to happen. E.g.:
  24 + # The expiration time is 100 days and an occurrence happened 3 days ago.
  25 + # Therefore the score is 97. Them we sum every score to get the total score
  26 + # for a search term.
  27 + def self.occurrences_scores
  28 + ActiveSupport::OrderedHash[*ActiveRecord::Base.connection.execute(
  29 + joins(:occurrences).
  30 + select("search_terms.id, sum(#{SearchTermOccurrence::EXPIRATION_TIME.to_i} - extract(epoch from (now() - search_term_occurrences.created_at))) as value").
  31 + where("search_term_occurrences.created_at > ?", DateTime.now - SearchTermOccurrence::EXPIRATION_TIME).
  32 + group("search_terms.id").
  33 + order('value DESC').
  34 + to_sql
  35 + ).map {|result| [result['id'].to_i, result['value'].to_i]}.flatten]
  36 + end
  37 +
  38 + def calculate_occurrence(occurrences_scores)
  39 + max_score = occurrences_scores.first[1]
  40 + (occurrences_scores[id]/max_score.to_f)*100
  41 + end
  42 +
  43 + def calculate_relevance(valid_occurrences)
  44 + indexed = valid_occurrences.last.indexed.to_f
  45 + return 0 if indexed == 0
  46 + total = valid_occurrences.last.total.to_f
  47 + (1 - indexed/total)*100
  48 + end
  49 +
  50 + def calculate_score(occurrences_scores)
  51 + valid_occurrences = occurrences.valid
  52 + if valid_occurrences.present?
  53 + # These scores vary from 1~100
  54 + self.occurrence_score = calculate_occurrence(occurrences_scores)
  55 + self.relevance_score = calculate_relevance(valid_occurrences)
  56 + else
  57 + self.occurrence_score = 0
  58 + self.relevance_score = 0
  59 + end
  60 + self.score = (occurrence_score * relevance_score)/100.0
  61 + self.save!
  62 + end
  63 +end
... ...
app/models/search_term_occurrence.rb 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +class SearchTermOccurrence < ActiveRecord::Base
  2 + belongs_to :search_term
  3 + validates_presence_of :search_term
  4 + attr_accessible :search_term, :created_at, :total, :indexed
  5 +
  6 + EXPIRATION_TIME = 1.year
  7 +
  8 + scope :valid, :conditions => ["search_term_occurrences.created_at > ?", DateTime.now - EXPIRATION_TIME]
  9 +end
... ...
app/models/suggestion_connection.rb 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +class SuggestionConnection < ActiveRecord::Base
  2 + attr_accessible :suggestion, :connection_type, :connection_id
  3 +
  4 + belongs_to :suggestion, :class_name => 'ProfileSuggestion', :foreign_key => 'suggestion_id'
  5 + belongs_to :connection, :polymorphic => true
  6 +end
... ...
app/models/task.rb
... ... @@ -73,10 +73,6 @@ class Task &lt; ActiveRecord::Base
73 73 end
74 74 end
75 75  
76   - def self.all_types
77   - %w[Invitation EnterpriseActivation AddMember Ticket SuggestArticle AddFriend CreateCommunity AbuseComplaint ApproveComment ApproveArticle CreateEnterprise ChangePassword EmailActivation InviteFriend InviteMember]
78   - end
79   -
80 76 # this method finished the task. It calls #perform, which must be overriden
81 77 # by subclasses. At the end a message (as returned by #finish_message) is
82 78 # sent to the requestor with #notify_requestor.
... ... @@ -254,6 +250,10 @@ class Task &lt; ActiveRecord::Base
254 250 { :conditions => [environment_condition, profile_condition].compact.join(' OR ') }
255 251 }
256 252  
  253 + def self.pending_types_for(profile)
  254 + Task.to(profile).pending.select('distinct type').map { |t| [t.class.name, t.title] }
  255 + end
  256 +
257 257 def opened?
258 258 status == Task::Status::ACTIVE || status == Task::Status::HIDDEN
259 259 end
... ... @@ -285,8 +285,9 @@ class Task &lt; ActiveRecord::Base
285 285 # If
286 286 def send_notification(action)
287 287 if sends_email?
288   - if self.requestor
289   - TaskMailer.generic_message("task_#{action}", self)
  288 + if self.requestor && !self.requestor.notification_emails.empty?
  289 + message = TaskMailer.generic_message("task_#{action}", self)
  290 + message.deliver if message
290 291 end
291 292 end
292 293 end
... ...