Commit 9508041164c47c02745ffce877414b9cb58495f9

Authored by Rodrigo Souto
2 parents d0b4277a f1e0dc83

Merge branch 'master' into language-selection

Conflicts:
	app/helpers/forms_helper.rb
	app/models/box.rb
	app/views/box_organizer/edit.rhtml
	db/schema.rb
	test/unit/box_test.rb
Showing 273 changed files with 8742 additions and 1192 deletions   Show diff stats

Too many changes.

To preserve performance only 100 of 273 files displayed.

INSTALL.varnish
... ... @@ -15,6 +15,10 @@ Noosfero was tested with Varnish 2.x. If you are using a Debian Lenny (and you
15 15 should, unless Debian already released Squeeze by now), make sure you install
16 16 varnish from the lenny-backports suite.
17 17  
  18 +Install the RPAF apache module (or skip this step if not using apache):
  19 +
  20 + # apt-get install libapache2-mod-rpaf
  21 +
18 22 3) Enable varnish logging:
19 23  
20 24 3a) Edit /etc/default/varnishncsa and uncomment the line that contains:
... ...
app/controllers/admin/users_controller.rb
... ... @@ -7,11 +7,12 @@ class UsersController < AdminController
7 7 format.html
8 8 format.xml do
9 9 @users = User.find(:all, :conditions => {:environment_id => environment.id}, :include => [:person])
10   - render :xml => @users.to_xml(
11   - :skip_types => true,
12   - :only => %w[email login created_at updated_at],
13   - :include => { :person => {:only => %w[name updated_at created_at address birth_date contact_phone identifier lat lng] } }
14   - )
  10 + send_data @users.to_xml(
  11 + :skip_types => true,
  12 + :only => %w[email login created_at updated_at],
  13 + :include => { :person => {:only => %w[name updated_at created_at address birth_date contact_phone identifier lat lng] } }),
  14 + :type => 'text/xml',
  15 + :disposition => "attachment; filename=users.xml"
15 16 end
16 17 format.csv do
17 18 @users = User.find(:all, :conditions => {:environment_id => environment.id}, :include => [:person])
... ...
app/controllers/application_controller.rb
... ... @@ -101,9 +101,10 @@ class ApplicationController < ActionController::Base
101 101 end
102 102 end
103 103  
  104 + include Noosfero::Plugin::HotSpot
  105 +
104 106 def init_noosfero_plugins
105   - @plugins = Noosfero::Plugin::Manager.new(self)
106   - @plugins.each do |plugin|
  107 + plugins.each do |plugin|
107 108 prepend_view_path(plugin.class.view_path)
108 109 end
109 110 init_noosfero_plugins_controller_filters
... ... @@ -112,8 +113,10 @@ class ApplicationController < ActionController::Base
112 113 # This is a generic method that initialize any possible filter defined by a
113 114 # plugin to the current controller being initialized.
114 115 def init_noosfero_plugins_controller_filters
115   - @plugins.each do |plugin|
116   - plugin.send(self.class.name.underscore + '_filters').each do |plugin_filter|
  116 + plugins.each do |plugin|
  117 + filters = plugin.send(self.class.name.underscore + '_filters')
  118 + filters = [filters] if !filters.kind_of?(Array)
  119 + filters.each do |plugin_filter|
117 120 self.class.send(plugin_filter[:type], plugin.class.name.underscore + '_' + plugin_filter[:method_name], (plugin_filter[:options] || {}))
118 121 self.class.send(:define_method, plugin.class.name.underscore + '_' + plugin_filter[:method_name], plugin_filter[:block])
119 122 end
... ...
app/controllers/box_organizer_controller.rb
... ... @@ -68,7 +68,8 @@ class BoxOrganizerController < ApplicationController
68 68 raise ArgumentError.new("Type %s is not allowed. Go away." % type)
69 69 end
70 70 else
71   - @block_types = available_blocks
  71 + @center_block_types = Box.acceptable_center_blocks & available_blocks
  72 + @side_block_types = Box.acceptable_side_blocks & available_blocks
72 73 @boxes = boxes_holder.boxes
73 74 render :action => 'add_block', :layout => false
74 75 end
... ...
app/controllers/my_profile/profile_design_controller.rb
... ... @@ -5,7 +5,7 @@ class ProfileDesignController < BoxOrganizerController
5 5 protect 'edit_profile_design', :profile
6 6  
7 7 def available_blocks
8   - blocks = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock, FeedReaderBlock, ProfileImageBlock, LocationBlock, SlideshowBlock, ProfileSearchBlock ]
  8 + blocks = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock, FeedReaderBlock, ProfileImageBlock, LocationBlock, SlideshowBlock, ProfileSearchBlock, HighlightsBlock ]
9 9  
10 10 # blocks exclusive for organizations
11 11 if profile.has_members?
... ...
app/controllers/my_profile/spam_controller.rb 0 → 100644
... ... @@ -0,0 +1,40 @@
  1 +class SpamController < MyProfileController
  2 +
  3 + protect :moderate_comments, :profile
  4 +
  5 + def index
  6 + if request.post?
  7 + begin
  8 + # FIXME duplicated logic
  9 + #
  10 + # This logic more or less replicates what is already in
  11 + # ContentViewerController#view_page,
  12 + # ContentViewerController#remove_comment and
  13 + # ContentViewerController#mark_comment_as_spam
  14 + if params[:remove_comment]
  15 + profile.comments_received.find(params[:remove_comment]).destroy
  16 + end
  17 + if params[:mark_comment_as_ham]
  18 + profile.comments_received.find(params[:mark_comment_as_ham]).ham!
  19 + end
  20 + if request.xhr?
  21 + json_response(true)
  22 + else
  23 + redirect_to :action => :index
  24 + end
  25 + rescue
  26 + json_response(false)
  27 + end
  28 + return
  29 + end
  30 +
  31 + @spam = profile.comments_received.spam.paginate({:page => params[:page]})
  32 + end
  33 +
  34 + protected
  35 +
  36 + def json_response(status)
  37 + render :text => {'ok' => status }.to_json, :content_type => 'application/json'
  38 + end
  39 +
  40 +end
... ...
app/controllers/public/account_controller.rb
... ... @@ -25,11 +25,13 @@ class AccountController &lt; ApplicationController
25 25  
26 26 # action to perform login to the application
27 27 def login
28   - @user = User.new
29   - @person = @user.build_person
30 28 store_location(request.referer) unless session[:return_to]
31 29 return unless request.post?
32   - self.current_user = User.authenticate(params[:user][:login], params[:user][:password], environment) if params[:user]
  30 +
  31 + self.current_user = plugins_alternative_authentication
  32 +
  33 + self.current_user ||= User.authenticate(params[:user][:login], params[:user][:password], environment) if params[:user]
  34 +
33 35 if logged_in?
34 36 if params[:remember_me] == "1"
35 37 self.current_user.remember_me
... ... @@ -41,7 +43,6 @@ class AccountController &lt; ApplicationController
41 43 end
42 44 else
43 45 session[:notice] = _('Incorrect username or password') if redirect?
44   - redirect_to :back if redirect?
45 46 end
46 47 end
47 48  
... ... @@ -56,6 +57,11 @@ class AccountController &lt; ApplicationController
56 57  
57 58 # action to register an user to the application
58 59 def signup
  60 + if @plugins.dispatch(:allow_user_registration).include?(false)
  61 + redirect_back_or_default(:controller => 'home')
  62 + session[:notice] = _("This environment doesn't allow user registration.")
  63 + end
  64 +
59 65 @invitation_code = params[:invitation_code]
60 66 begin
61 67 if params[:user]
... ... @@ -125,6 +131,10 @@ class AccountController &lt; ApplicationController
125 131 #
126 132 # Posts back.
127 133 def forgot_password
  134 + if @plugins.dispatch(:allow_password_recovery).include?(false)
  135 + redirect_back_or_default(:controller => 'home')
  136 + session[:notice] = _("This environment doesn't allow password recovery.")
  137 + end
128 138 @change_password = ChangePassword.new(params[:change_password])
129 139  
130 140 if request.post?
... ... @@ -316,4 +326,13 @@ class AccountController &lt; ApplicationController
316 326 end
317 327 end
318 328  
  329 + def plugins_alternative_authentication
  330 + user = nil
  331 + @plugins.each do |plugin|
  332 + user = plugin.alternative_authentication
  333 + break unless user.nil?
  334 + end
  335 + user
  336 + end
  337 +
319 338 end
... ...
app/controllers/public/content_viewer_controller.rb
... ... @@ -2,6 +2,8 @@ class ContentViewerController &lt; ApplicationController
2 2  
3 3 needs_profile
4 4  
  5 + before_filter :comment_author, :only => :edit_comment
  6 +
5 7 helper ProfileHelper
6 8 helper TagsHelper
7 9  
... ... @@ -19,7 +21,7 @@ class ContentViewerController &lt; ApplicationController
19 21 unless @page
20 22 page_from_old_path = profile.articles.find_by_old_path(path)
21 23 if page_from_old_path
22   - redirect_to :profile => profile.identifier, :page => page_from_old_path.explode_path
  24 + redirect_to profile.url.merge(:page => page_from_old_path.explode_path)
23 25 return
24 26 end
25 27 end
... ... @@ -75,8 +77,14 @@ class ContentViewerController &lt; ApplicationController
75 77 @comment = Comment.new
76 78 end
77 79  
78   - if request.post? && params[:remove_comment]
79   - remove_comment
  80 + if request.post?
  81 + if params[:remove_comment]
  82 + remove_comment
  83 + return
  84 + elsif params[:mark_comment_as_spam]
  85 + mark_comment_as_spam
  86 + return
  87 + end
80 88 end
81 89  
82 90 if @page.has_posts?
... ... @@ -107,23 +115,46 @@ class ContentViewerController &lt; ApplicationController
107 115 end
108 116 end
109 117  
110   - @comments = @page.comments(true).as_thread
111   - @comments_count = @page.comments.count
  118 + comments = @page.comments.without_spam
  119 + @comments = comments.as_thread
  120 + @comments_count = comments.count
112 121 if params[:slideshow]
113 122 render :action => 'slideshow', :layout => 'slideshow'
114 123 end
115 124 end
116 125  
  126 + def edit_comment
  127 + path = params[:page].join('/')
  128 + @page = profile.articles.find_by_path(path)
  129 + @form_div = 'opened'
  130 + @comment = @page.comments.find_by_id(params[:id])
  131 + if @comment
  132 + if request.post?
  133 + begin
  134 + @comment.update_attributes(params[:comment])
  135 + session[:notice] = _('Comment succesfully updated')
  136 + redirect_to :action => 'view_page', :profile => profile.identifier, :page => @comment.article.explode_path
  137 + rescue
  138 + session[:notice] = _('Comment could not be updated')
  139 + end
  140 + end
  141 + else
  142 + redirect_to @page.view_url
  143 + session[:notice] = _('Could not find the comment in the article')
  144 + end
  145 + end
  146 +
117 147 protected
118 148  
119 149 def add_comment
120 150 @comment.author = user if logged_in?
121 151 @comment.article = @page
122 152 @comment.ip_address = request.remote_ip
  153 + @comment.user_agent = request.user_agent
  154 + @comment.referrer = request.referrer
123 155 plugins_filter_comment(@comment)
124 156 return if @comment.rejected?
125 157 if (pass_without_comment_captcha? || verify_recaptcha(:model => @comment, :message => _('Please type the words correctly'))) && @comment.save
126   - plugins_comment_saved(@comment)
127 158 @page.touch
128 159 @comment = nil # clear the comment form
129 160 redirect_to :action => 'view_page', :profile => params[:profile], :page => @page.explode_path, :view => params[:view]
... ... @@ -138,12 +169,6 @@ class ContentViewerController &lt; ApplicationController
138 169 end
139 170 end
140 171  
141   - def plugins_comment_saved(comment)
142   - @plugins.each do |plugin|
143   - plugin.comment_saved(comment)
144   - end
145   - end
146   -
147 172 def pass_without_comment_captcha?
148 173 logged_in? && !environment.enabled?('captcha_for_logged_users')
149 174 end
... ... @@ -153,9 +178,24 @@ class ContentViewerController &lt; ApplicationController
153 178 @comment = @page.comments.find(params[:remove_comment])
154 179 if (user == @comment.author || user == @page.profile || user.has_permission?(:moderate_comments, @page.profile))
155 180 @comment.destroy
156   - session[:notice] = _('Comment succesfully deleted')
157 181 end
158   - redirect_to :action => 'view_page', :profile => params[:profile], :page => @page.explode_path, :view => params[:view]
  182 + finish_comment_handling
  183 + end
  184 +
  185 + def mark_comment_as_spam
  186 + @comment = @page.comments.find(params[:mark_comment_as_spam])
  187 + if logged_in? && (user == @page.profile || user.has_permission?(:moderate_comments, @page.profile))
  188 + @comment.spam!
  189 + end
  190 + finish_comment_handling
  191 + end
  192 +
  193 + def finish_comment_handling
  194 + if request.xhr?
  195 + render :text => {'ok' => true}.to_json, :content_type => 'application/json'
  196 + else
  197 + redirect_to :action => 'view_page', :profile => params[:profile], :page => @page.explode_path, :view => params[:view]
  198 + end
159 199 end
160 200  
161 201 def per_page
... ... @@ -181,4 +221,13 @@ class ContentViewerController &lt; ApplicationController
181 221 end
182 222 end
183 223  
  224 + def comment_author
  225 + comment = Comment.find_by_id(params[:id])
  226 + if comment
  227 + render_access_denied if comment.author.blank? || comment.author != user
  228 + else
  229 + render_not_found
  230 + end
  231 + end
  232 +
184 233 end
... ...
app/controllers/public/profile_controller.rb
... ... @@ -212,9 +212,9 @@ class ProfileController &lt; PublicController
212 212 begin
213 213 scrap = current_user.person.scraps(params[:scrap_id])
214 214 scrap.destroy
215   - render :text => _('Scrap successfully removed.')
  215 + finish_successful_removal 'Scrap successfully removed.'
216 216 rescue
217   - render :text => _('You could not remove this scrap')
  217 + finish_unsuccessful_removal 'You could not remove this scrap.'
218 218 end
219 219 end
220 220  
... ... @@ -227,9 +227,9 @@ class ProfileController &lt; PublicController
227 227 else
228 228 activity.destroy
229 229 end
230   - render :text => _('Activity successfully removed.')
  230 + finish_successful_removal 'Activity successfully removed.'
231 231 rescue
232   - render :text => _('You could not remove this activity')
  232 + finish_unsuccessful_removal 'You could not remove this activity.'
233 233 end
234 234 end
235 235  
... ... @@ -244,6 +244,24 @@ class ProfileController &lt; PublicController
244 244 end
245 245 end
246 246  
  247 + def finish_successful_removal(msg)
  248 + if request.xhr?
  249 + render :text => {'ok' => true}.to_json, :content_type => 'application/json'
  250 + else
  251 + session[:notice] = _(msg)
  252 + redirect_to :action => :index
  253 + end
  254 + end
  255 +
  256 + def finish_unsuccessful_removal(msg)
  257 + session[:notice] = _(msg)
  258 + if request.xhr?
  259 + render :text => {'redirect' => url_for(:action => :index)}.to_json, :content_type => 'application/json'
  260 + else
  261 + redirect_to :action => :index
  262 + end
  263 + end
  264 +
247 265 def profile_info
248 266 begin
249 267 @block = profile.blocks.find(params[:block_id])
... ... @@ -303,9 +321,10 @@ class ProfileController &lt; PublicController
303 321 @comment = Comment.find(params[:comment_id])
304 322 if (user == @comment.author || user == profile || user.has_permission?(:moderate_comments, profile))
305 323 @comment.destroy
306   - session[:notice] = _('Comment successfully deleted')
  324 + finish_successful_removal 'Comment successfully removed.'
  325 + else
  326 + finish_unsuccessful_removal 'You could not remove this comment.'
307 327 end
308   - redirect_to :action => :index
309 328 end
310 329  
311 330 protected
... ...
app/controllers/public/search_controller.rb
... ... @@ -4,10 +4,17 @@ class SearchController &lt; PublicController
4 4 include SearchHelper
5 5 include ActionView::Helpers::NumberHelper
6 6  
  7 + before_filter :redirect_asset_param, :except => [:facets_browse, :assets]
7 8 before_filter :load_category
8 9 before_filter :load_search_assets
9 10 before_filter :load_query
10 11  
  12 + # Backwards compatibility with old URLs
  13 + def redirect_asset_param
  14 + return unless params.has_key?(:asset)
  15 + redirect_to params.merge(:action => params.delete(:asset))
  16 + end
  17 +
11 18 no_design_blocks
12 19  
13 20 def facets_browse
... ... @@ -250,10 +257,9 @@ class SearchController &lt; PublicController
250 257 end
251 258  
252 259 def limit
253   - searching = @searching.values.select{ |v| v }
254   - if params[:display] == 'map'
  260 + if map_search?
255 261 MAP_SEARCH_LIMIT
256   - elsif searching.size <= 1
  262 + elsif !multiple_search?
257 263 if [:people, :communities].include? @asset
258 264 BLOCKS_SEARCH_LIMIT
259 265 elsif @asset == :enterprises and @empty_query
... ... @@ -267,31 +273,34 @@ class SearchController &lt; PublicController
267 273 end
268 274  
269 275 def paginate_options(page = params[:page])
  276 + page = 1 if multiple_search? or params[:display] == 'map'
270 277 { :per_page => limit, :page => page }
271 278 end
272 279  
273 280 def full_text_search(filters = [], options = {})
274 281 paginate_options = paginate_options(params[:page])
275 282 asset_class = asset_class(@asset)
276   -
277 283 solr_options = options
278   - if !@results_only and asset_class.respond_to? :facets
279   - solr_options.merge! asset_class.facets_find_options(params[:facet])
280   - solr_options[:all_facets] = true
281   - solr_options[:limit] = 0 if @facets_only
282   - end
283   - solr_options[:filter_queries] ||= []
284   - solr_options[:filter_queries] += filters
285   - solr_options[:filter_queries] << "environment_id:#{environment.id}"
286   - solr_options[:filter_queries] << asset_class.facet_category_query.call(@category) if @category
287   -
288   - solr_options[:boost_functions] ||= []
289   - params[:order_by] = nil if params[:order_by] == 'none'
290   - if params[:order_by]
291   - order = SortOptions[@asset][params[:order_by].to_sym]
292   - raise "Unknown order by" if order.nil?
293   - order[:solr_opts].each do |opt, value|
294   - solr_options[opt] = value.is_a?(Proc) ? instance_eval(&value) : value
  284 + pg_options = paginate_options(params[:page])
  285 +
  286 + if !multiple_search?
  287 + if !@results_only and asset_class.respond_to? :facets
  288 + solr_options.merge! asset_class.facets_find_options(params[:facet])
  289 + solr_options[:all_facets] = true
  290 + end
  291 + solr_options[:filter_queries] ||= []
  292 + solr_options[:filter_queries] += filters
  293 + solr_options[:filter_queries] << "environment_id:#{environment.id}"
  294 + solr_options[:filter_queries] << asset_class.facet_category_query.call(@category) if @category
  295 +
  296 + solr_options[:boost_functions] ||= []
  297 + params[:order_by] = nil if params[:order_by] == 'none'
  298 + if params[:order_by]
  299 + order = SortOptions[@asset][params[:order_by].to_sym]
  300 + raise "Unknown order by" if order.nil?
  301 + order[:solr_opts].each do |opt, value|
  302 + solr_options[opt] = value.is_a?(Proc) ? instance_eval(&value) : value
  303 + end
295 304 end
296 305 end
297 306  
... ...
app/helpers/application_helper.rb
... ... @@ -265,9 +265,9 @@ module ApplicationHelper
265 265  
266 266 VIEW_EXTENSIONS = %w[.rhtml .html.erb]
267 267  
268   - def partial_for_class_in_view_path(klass, view_path)
  268 + def partial_for_class_in_view_path(klass, view_path, suffix = nil)
269 269 return nil if klass.nil?
270   - name = klass.name.underscore
  270 + name = [klass.name.underscore, suffix].compact.map(&:to_s).join('_')
271 271  
272 272 search_name = String.new(name)
273 273 if search_name.include?("/")
... ... @@ -285,28 +285,17 @@ module ApplicationHelper
285 285 partial_for_class_in_view_path(klass.superclass, view_path)
286 286 end
287 287  
288   - def partial_for_class(klass)
  288 + def partial_for_class(klass, suffix=nil)
289 289 raise ArgumentError, 'No partial for object. Is there a partial for any class in the inheritance hierarchy?' if klass.nil?
290 290 name = klass.name.underscore
291 291 @controller.view_paths.each do |view_path|
292   - partial = partial_for_class_in_view_path(klass, view_path)
  292 + partial = partial_for_class_in_view_path(klass, view_path, suffix)
293 293 return partial if partial
294 294 end
295 295  
296 296 raise ArgumentError, 'No partial for object. Is there a partial for any class in the inheritance hierarchy?'
297 297 end
298 298  
299   - def partial_for_task_class(klass, action)
300   - raise ArgumentError, 'No partial for object. Is there a partial for any class in the inheritance hierarchy?' if klass.nil?
301   -
302   - name = "#{klass.name.underscore}_#{action.to_s}"
303   - VIEW_EXTENSIONS.each do |ext|
304   - return name if File.exists?(File.join(RAILS_ROOT, 'app', 'views', params[:controller], '_'+name+ext))
305   - end
306   -
307   - partial_for_task_class(klass.superclass, action)
308   - end
309   -
310 299 def view_for_profile_actions(klass)
311 300 raise ArgumentError, 'No profile actions view for this class.' if klass.nil?
312 301  
... ... @@ -1336,6 +1325,19 @@ module ApplicationHelper
1336 1325 end
1337 1326 end
1338 1327  
  1328 + def expirable_link_to(expired, content, url, options = {})
  1329 + if expired
  1330 + options[:class] = (options[:class] || '') + ' disabled'
  1331 + content_tag('a', '&nbsp;'+content_tag('span', content), options)
  1332 + else
  1333 + link_to content, url, options
  1334 + end
  1335 + end
  1336 +
  1337 + def remove_content_button(action)
  1338 + @plugins.dispatch("content_remove_#{action.to_s}", @page).include?(true)
  1339 + end
  1340 +
1339 1341 def template_options(klass, field_name)
1340 1342 return '' if klass.templates.count == 0
1341 1343 return hidden_field_tag("#{field_name}[template_id]", klass.templates.first.id) if klass.templates.count == 1
... ... @@ -1401,4 +1403,19 @@ module ApplicationHelper
1401 1403 result
1402 1404 end
1403 1405  
  1406 + def expirable_content_reference(content, action, text, url, options = {})
  1407 + reason = @plugins.dispatch("content_expire_#{action.to_s}", content).first
  1408 + options[:title] = reason
  1409 + expirable_link_to reason.present?, text, url, options
  1410 + end
  1411 +
  1412 + def expirable_button(content, action, text, url, options = {})
  1413 + options[:class] = "button with-text icon-#{action.to_s}"
  1414 + expirable_content_reference content, action, text, url, options
  1415 + end
  1416 +
  1417 + def expirable_comment_link(content, action, text, url, options = {})
  1418 + options[:class] = "comment-footer comment-footer-link comment-footer-hide"
  1419 + expirable_content_reference content, action, text, url, options
  1420 + end
1404 1421 end
... ...
app/helpers/boxes_helper.rb
... ... @@ -162,9 +162,6 @@ module BoxesHelper
162 162 #
163 163 # +box+ is always needed
164 164 def block_target(box, block = nil)
165   - # FIXME hardcoded
166   - return '' if box.position == 1
167   -
168 165 id =
169 166 if block.nil?
170 167 "end-of-box-#{box.id}"
... ... @@ -172,14 +169,11 @@ module BoxesHelper
172 169 "before-block-#{block.id}"
173 170 end
174 171  
175   - content_tag('div', '&nbsp;', :id => id, :class => 'block-target' ) + drop_receiving_element(id, :url => { :action => 'move_block', :target => id }, :accept => 'block', :hoverclass => 'block-target-hover')
  172 + 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')
176 173 end
177 174  
178 175 # makes the given block draggable so it can be moved away.
179 176 def block_handle(block)
180   - # FIXME hardcoded
181   - return '' if block.box.position == 1
182   -
183 177 draggable_element("block-#{block.id}", :revert => true)
184 178 end
185 179  
... ... @@ -211,7 +205,7 @@ module BoxesHelper
211 205 end
212 206  
213 207 if block.editable?
214   - buttons << lightbox_icon_button(:edit, _('Edit'), { :action => 'edit', :id => block.id })
  208 + buttons << colorbox_icon_button(:edit, _('Edit'), { :action => 'edit', :id => block.id })
215 209 end
216 210  
217 211 if !block.main?
... ...
app/helpers/cms_helper.rb
... ... @@ -42,13 +42,25 @@ module CmsHelper
42 42  
43 43 def display_spread_button(profile, article)
44 44 if profile.person?
45   - button_without_text :spread, _('Spread this'), :action => 'publish', :id => article.id
  45 + expirable_button article, :spread, _('Spread this'), :action => 'publish', :id => article.id
46 46 elsif profile.community? && environment.portal_community
47   - button_without_text :spread, _('Spread this'), :action => 'publish_on_portal_community', :id => article.id
  47 + expirable_button article, :spread, _('Spread this'), :action => 'publish_on_portal_community', :id => article.id
48 48 end
49 49 end
50 50  
51 51 def display_delete_button(article)
52   - button_without_text :delete, _('Delete'), { :action => 'destroy', :id => article.id }, :method => :post, :confirm => delete_article_message(article)
  52 + expirable_button article, :delete, _('Delete'), { :action => 'destroy', :id => article.id }, :method => :post, :confirm => delete_article_message(article)
  53 + end
  54 +
  55 + def expirable_button(content, action, title, url, options = {})
  56 + reason = @plugins.dispatch("content_expire_#{action.to_s}", content).first
  57 + if reason.present?
  58 + options[:class] = (options[:class] || '') + ' disabled'
  59 + options[:disabled] = 'disabled'
  60 + options.delete(:confirm)
  61 + options.delete(:method)
  62 + title = reason
  63 + end
  64 + button_without_text action.to_sym, title, url, options
53 65 end
54 66 end
... ...
app/helpers/colorbox_helper.rb
... ... @@ -8,6 +8,10 @@ module ColorboxHelper
8 8 button(type, label, url, colorbox_options(options))
9 9 end
10 10  
  11 + def colorbox_icon_button(type, label, url, options = {})
  12 + icon_button(type, label, url, colorbox_options(options))
  13 + end
  14 +
11 15 # options must be an HTML options hash as passed to link_to etc.
12 16 #
13 17 # returns a new hash with colorbox class added. Keeps existing classes.
... ...
app/helpers/content_viewer_helper.rb
... ... @@ -4,11 +4,11 @@ module ContentViewerHelper
4 4 include ForumHelper
5 5  
6 6 def number_of_comments(article)
7   - n = article.comments.size
  7 + n = article.comments.without_spam.count
8 8 if n == 0
9 9 _('No comments yet')
10 10 else
11   - n_('One comment', '%{comments} comments', n) % { :comments => n }
  11 + n_('One comment', '<span class="comment-count">%{comments}</span> comments', n) % { :comments => n }
12 12 end
13 13 end
14 14  
... ...
app/helpers/forms_helper.rb
... ... @@ -142,6 +142,119 @@ module FormsHelper
142 142 content_tag('table',rows.join("\n"))
143 143 end
144 144  
  145 + def date_field(name, value, format = '%Y-%m-%d', datepicker_options = {}, html_options = {})
  146 + datepicker_options[:disabled] ||= false
  147 + datepicker_options[:alt_field] ||= ''
  148 + datepicker_options[:alt_format] ||= ''
  149 + datepicker_options[:append_text] ||= ''
  150 + datepicker_options[:auto_size] ||= false
  151 + datepicker_options[:button_image] ||= ''
  152 + datepicker_options[:button_image_only] ||= false
  153 + datepicker_options[:button_text] ||= '...'
  154 + datepicker_options[:calculate_week] ||= 'jQuery.datepicker.iso8601Week'
  155 + datepicker_options[:change_month] ||= false
  156 + datepicker_options[:change_year] ||= false
  157 + datepicker_options[:close_text] ||= _('Done')
  158 + datepicker_options[:constrain_input] ||= true
  159 + datepicker_options[:current_text] ||= _('Today')
  160 + datepicker_options[:date_format] ||= 'mm/dd/yy'
  161 + datepicker_options[:day_names] ||= [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')]
  162 + datepicker_options[:day_names_min] ||= [_('Su'), _('Mo'), _('Tu'), _('We'), _('Th'), _('Fr'), _('Sa')]
  163 + datepicker_options[:day_names_short] ||= [_('Sun'), _('Mon'), _('Tue'), _('Wed'), _('Thu'), _('Fri'), _('Sat')]
  164 + datepicker_options[:default_date] ||= nil
  165 + datepicker_options[:duration] ||= 'normal'
  166 + datepicker_options[:first_day] ||= 0
  167 + datepicker_options[:goto_current] ||= false
  168 + datepicker_options[:hide_if_no_prev_next] ||= false
  169 + datepicker_options[:is_rtl] ||= false
  170 + datepicker_options[:max_date] ||= nil
  171 + datepicker_options[:min_date] ||= nil
  172 + datepicker_options[:month_names] ||= [_('January'), _('February'), _('March'), _('April'), _('May'), _('June'), _('July'), _('August'), _('September'), _('October'), _('November'), _('December')]
  173 + datepicker_options[:month_names_short] ||= [_('Jan'), _('Feb'), _('Mar'), _('Apr'), _('May'), _('Jun'), _('Jul'), _('Aug'), _('Sep'), _('Oct'), _('Nov'), _('Dec')]
  174 + datepicker_options[:navigation_as_date_format] ||= false
  175 + datepicker_options[:next_text] ||= _('Next')
  176 + datepicker_options[:number_of_months] ||= 1
  177 + datepicker_options[:prev_text] ||= _('Prev')
  178 + datepicker_options[:select_other_months] ||= false
  179 + datepicker_options[:short_year_cutoff] ||= '+10'
  180 + datepicker_options[:show_button_panel] ||= false
  181 + datepicker_options[:show_current_at_pos] ||= 0
  182 + datepicker_options[:show_month_after_year] ||= false
  183 + datepicker_options[:show_on] ||= 'focus'
  184 + datepicker_options[:show_options] ||= {}
  185 + datepicker_options[:show_other_months] ||= false
  186 + datepicker_options[:show_week] ||= false
  187 + datepicker_options[:step_months] ||= 1
  188 + datepicker_options[:week_header] ||= _('Wk')
  189 + datepicker_options[:year_range] ||= 'c-10:c+10'
  190 + datepicker_options[:year_suffix] ||= ''
  191 +
  192 + element_id = html_options[:id] || 'datepicker-date'
  193 + value = value.strftime(format) if value.present?
  194 + method = datepicker_options[:time] ? 'datetimepicker' : 'datepicker'
  195 + result = text_field_tag(name, value, html_options)
  196 + result +=
  197 + "
  198 + <script type='text/javascript'>
  199 + jQuery('##{element_id}').#{method}({
  200 + disabled: #{datepicker_options[:disabled].to_json},
  201 + altField: #{datepicker_options[:alt_field].to_json},
  202 + altFormat: #{datepicker_options[:alt_format].to_json},
  203 + appendText: #{datepicker_options[:append_text].to_json},
  204 + autoSize: #{datepicker_options[:auto_size].to_json},
  205 + buttonImage: #{datepicker_options[:button_image].to_json},
  206 + buttonImageOnly: #{datepicker_options[:button_image_only].to_json},
  207 + buttonText: #{datepicker_options[:button_text].to_json},
  208 + calculateWeek: #{datepicker_options[:calculate_week].to_json},
  209 + changeMonth: #{datepicker_options[:change_month].to_json},
  210 + changeYear: #{datepicker_options[:change_year].to_json},
  211 + closeText: #{datepicker_options[:close_text].to_json},
  212 + constrainInput: #{datepicker_options[:constrain_input].to_json},
  213 + currentText: #{datepicker_options[:current_text].to_json},
  214 + dateFormat: #{datepicker_options[:date_format].to_json},
  215 + dayNames: #{datepicker_options[:day_names].to_json},
  216 + dayNamesMin: #{datepicker_options[:day_names_min].to_json},
  217 + dayNamesShort: #{datepicker_options[:day_names_short].to_json},
  218 + defaultDate: #{datepicker_options[:default_date].to_json},
  219 + duration: #{datepicker_options[:duration].to_json},
  220 + firstDay: #{datepicker_options[:first_day].to_json},
  221 + gotoCurrent: #{datepicker_options[:goto_current].to_json},
  222 + hideIfNoPrevNext: #{datepicker_options[:hide_if_no_prev_next].to_json},
  223 + isRTL: #{datepicker_options[:is_rtl].to_json},
  224 + maxDate: #{datepicker_options[:max_date].to_json},
  225 + minDate: #{datepicker_options[:min_date].to_json},
  226 + monthNames: #{datepicker_options[:month_names].to_json},
  227 + monthNamesShort: #{datepicker_options[:month_names_short].to_json},
  228 + navigationAsDateFormat: #{datepicker_options[:navigation_as_date_format].to_json},
  229 + nextText: #{datepicker_options[:next_text].to_json},
  230 + numberOfMonths: #{datepicker_options[:number_of_months].to_json},
  231 + prevText: #{datepicker_options[:prev_text].to_json},
  232 + selectOtherMonths: #{datepicker_options[:select_other_months].to_json},
  233 + shortYearCutoff: #{datepicker_options[:short_year_cutoff].to_json},
  234 + showButtonPanel: #{datepicker_options[:show_button_panel].to_json},
  235 + showCurrentAtPos: #{datepicker_options[:show_current_at_pos].to_json},
  236 + showMonthAfterYear: #{datepicker_options[:show_month_after_year].to_json},
  237 + showOn: #{datepicker_options[:show_on].to_json},
  238 + showOptions: #{datepicker_options[:show_options].to_json},
  239 + showOtherMonths: #{datepicker_options[:show_other_months].to_json},
  240 + showWeek: #{datepicker_options[:show_week].to_json},
  241 + stepMonths: #{datepicker_options[:step_months].to_json},
  242 + weekHeader: #{datepicker_options[:week_header].to_json},
  243 + yearRange: #{datepicker_options[:year_range].to_json},
  244 + yearSuffix: #{datepicker_options[:year_suffix].to_json}
  245 + })
  246 + </script>
  247 + "
  248 + result
  249 + end
  250 +
  251 + def date_range_field(from_name, to_name, from_value, to_value, format = '%Y-%m-%d', datepicker_options = {}, html_options = {})
  252 + from_id = html_options[:from_id] || 'datepicker-from-date'
  253 + to_id = html_options[:to_id] || 'datepicker-to-date'
  254 + return _('From') +' '+ date_field(from_name, from_value, format, datepicker_options, html_options.merge({:id => from_id})) +
  255 + ' ' + _('until') +' '+ date_field(to_name, to_value, format, datepicker_options, html_options.merge({:id => to_id}))
  256 + end
  257 +
145 258 protected
146 259 def self.next_id_number
147 260 if defined? @@id_num
... ...
app/helpers/search_helper.rb
... ... @@ -45,6 +45,14 @@ module SearchHelper
45 45 # FIXME remove it after search_controler refactored
46 46 include EventsHelper
47 47  
  48 + def multiple_search?
  49 + ['index', 'category_index'].include?(params[:action]) or @results.size > 1
  50 + end
  51 +
  52 + def map_search?
  53 + !@empty_query and !multiple_search? and params[:display] == 'map'
  54 + end
  55 +
48 56 def search_page_title(title, category = nil)
49 57 title = "<h1>" + title
50 58 title += '<small>' + category.name + '</small>' if category
... ... @@ -58,8 +66,8 @@ module SearchHelper
58 66 :align => 'center', :class => 'search-category-context') if category
59 67 end
60 68  
61   - def display_results(use_map = false)
62   - if params[:display] == 'map' && use_map
  69 + def display_results(map_capable = false)
  70 + if map_capable and map_search?
63 71 partial = 'google_maps'
64 72 klass = 'map'
65 73 else
... ... @@ -99,7 +107,7 @@ module SearchHelper
99 107 @asset_class = asset_class(asset)
100 108 render(:partial => 'facets_unselect_menu')
101 109 end
102   -
  110 +
103 111 def facet_javascript(input_id, facet, array)
104 112 array = [] if array.nil?
105 113 hintText = _('Type in an option')
... ... @@ -148,6 +156,7 @@ module SearchHelper
148 156 params = params.dup
149 157 params[:facet].each do |id, value|
150 158 facet = klass.facet_by_id(id.to_sym)
  159 + next unless facet
151 160 if value.kind_of?(Hash)
152 161 label_hash = facet[:label].call(environment)
153 162 value.each do |label_id, value|
... ...
app/models/box.rb
... ... @@ -6,4 +6,76 @@ class Box &lt; ActiveRecord::Base
6 6 def environment
7 7 owner ? (owner.kind_of?(Environment) ? owner : owner.environment) : nil
8 8 end
  9 +
  10 + def acceptable_blocks
  11 + to_css_class_name central? ? Box.acceptable_center_blocks : Box.acceptable_side_blocks
  12 + end
  13 +
  14 + def central?
  15 + position == 1
  16 + end
  17 +
  18 + def self.acceptable_center_blocks
  19 + [ ArticleBlock,
  20 + BlogArchivesBlock,
  21 + CategoriesBlock,
  22 + CommunitiesBlock,
  23 + EnterprisesBlock,
  24 + EnvironmentStatisticsBlock,
  25 + FansBlock,
  26 + FavoriteEnterprisesBlock,
  27 + FeedReaderBlock,
  28 + FriendsBlock,
  29 + HighlightsBlock,
  30 + LinkListBlock,
  31 + LoginBlock,
  32 + MainBlock,
  33 + MembersBlock,
  34 + MyNetworkBlock,
  35 + PeopleBlock,
  36 + ProfileImageBlock,
  37 + RawHTMLBlock,
  38 + RecentDocumentsBlock,
  39 + SellersSearchBlock,
  40 + TagsBlock ]
  41 + end
  42 +
  43 + def self.acceptable_side_blocks
  44 + [ ArticleBlock,
  45 + BlogArchivesBlock,
  46 + CategoriesBlock,
  47 + CommunitiesBlock,
  48 + DisabledEnterpriseMessageBlock,
  49 + EnterprisesBlock,
  50 + EnvironmentStatisticsBlock,
  51 + FansBlock,
  52 + FavoriteEnterprisesBlock,
  53 + FeaturedProductsBlock,
  54 + FeedReaderBlock,
  55 + FriendsBlock,
  56 + HighlightsBlock,
  57 + LinkListBlock,
  58 + LocationBlock,
  59 + LoginBlock,
  60 + MembersBlock,
  61 + MyNetworkBlock,
  62 + PeopleBlock,
  63 + ProductsBlock,
  64 + ProfileImageBlock,
  65 + ProfileInfoBlock,
  66 + ProfileSearchBlock,
  67 + RawHTMLBlock,
  68 + RecentDocumentsBlock,
  69 + SellersSearchBlock,
  70 + SlideshowBlock,
  71 + TagsBlock
  72 + ]
  73 + end
  74 +
  75 + private
  76 +
  77 + def to_css_class_name(blocks)
  78 + blocks.map{ |block| block.to_s.underscore.tr('_', '-') }
  79 + end
  80 +
9 81 end
... ...
app/models/comment.rb
... ... @@ -10,6 +10,9 @@ class Comment &lt; ActiveRecord::Base
10 10 has_many :children, :class_name => 'Comment', :foreign_key => 'reply_of_id', :dependent => :destroy
11 11 belongs_to :reply_of, :class_name => 'Comment', :foreign_key => 'reply_of_id'
12 12  
  13 + named_scope :without_spam, :conditions => ['spam IS NULL OR spam = ?', false]
  14 + named_scope :spam, :conditions => ['spam = ?', true]
  15 +
13 16 # unauthenticated authors:
14 17 validates_presence_of :name, :if => (lambda { |record| !record.email.blank? })
15 18 validates_presence_of :email, :if => (lambda { |record| !record.name.blank? })
... ... @@ -25,6 +28,8 @@ class Comment &lt; ActiveRecord::Base
25 28  
26 29 xss_terminate :only => [ :body, :title, :name ], :on => 'validation'
27 30  
  31 + delegate :environment, :to => :source
  32 +
28 33 def action_tracker_target
29 34 self.article.profile
30 35 end
... ... @@ -85,7 +90,28 @@ class Comment &lt; ActiveRecord::Base
85 90 end
86 91 end
87 92  
88   - after_create :notify_by_mail
  93 + after_create :schedule_notification
  94 +
  95 + def schedule_notification
  96 + Delayed::Job.enqueue CommentHandler.new(self.id, :verify_and_notify)
  97 + end
  98 +
  99 + delegate :environment, :to => :profile
  100 + delegate :profile, :to => :source
  101 +
  102 + include Noosfero::Plugin::HotSpot
  103 +
  104 + def verify_and_notify
  105 + check_for_spam
  106 + unless spam?
  107 + notify_by_mail
  108 + end
  109 + end
  110 +
  111 + def check_for_spam
  112 + plugins.dispatch(:check_comment_for_spam, self)
  113 + end
  114 +
89 115 def notify_by_mail
90 116 if source.kind_of?(Article) && article.notify_comments?
91 117 if !article.profile.notification_emails.empty?
... ... @@ -123,10 +149,14 @@ class Comment &lt; ActiveRecord::Base
123 149 def self.as_thread
124 150 result = {}
125 151 root = []
126   - all.each do |c|
  152 + order(:id).each do |c|
127 153 c.replies = []
128 154 result[c.id] ||= c
129   - c.reply_of_id.nil? ? root << c : result[c.reply_of_id].replies << c
  155 + if result[c.reply_of_id]
  156 + result[c.reply_of_id].replies << c
  157 + else
  158 + root << c
  159 + end
130 160 end
131 161 root
132 162 end
... ... @@ -183,4 +213,34 @@ class Comment &lt; ActiveRecord::Base
183 213 @rejected = true
184 214 end
185 215  
  216 + def spam?
  217 + !spam.nil? && spam
  218 + end
  219 +
  220 + def ham?
  221 + !spam.nil? && !spam
  222 + end
  223 +
  224 + def spam!
  225 + self.spam = true
  226 + self.save!
  227 + Delayed::Job.enqueue(CommentHandler.new(self.id, :marked_as_spam))
  228 + self
  229 + end
  230 +
  231 + def ham!
  232 + self.spam = false
  233 + self.save!
  234 + Delayed::Job.enqueue(CommentHandler.new(self.id, :marked_as_ham))
  235 + self
  236 + end
  237 +
  238 + def marked_as_spam
  239 + plugins.dispatch(:comment_marked_as_spam, self)
  240 + end
  241 +
  242 + def marked_as_ham
  243 + plugins.dispatch(:comment_marked_as_ham, self)
  244 + end
  245 +
186 246 end
... ...
app/models/comment_handler.rb 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +class CommentHandler < Struct.new(:comment_id, :method)
  2 +
  3 + def perform
  4 + comment = Comment.find(comment_id)
  5 + comment.send(method)
  6 + rescue ActiveRecord::RecordNotFound
  7 + # just ignore non-existing comments
  8 + end
  9 +
  10 +end
... ...
app/models/community.rb
... ... @@ -88,7 +88,7 @@ class Community &lt; Organization
88 88 end
89 89  
90 90 def activities
91   - Scrap.find_by_sql("SELECT id, updated_at, '#{Scrap.to_s}' AS klass FROM #{Scrap.table_name} WHERE scraps.receiver_id = #{self.id} AND scraps.scrap_id IS NULL UNION SELECT id, updated_at, '#{ActionTracker::Record.to_s}' AS klass FROM #{ActionTracker::Record.table_name} WHERE action_tracker.target_id = #{self.id} UNION SELECT at.id, at.updated_at, '#{ActionTracker::Record.to_s}' AS klass FROM #{ActionTracker::Record.table_name} at INNER JOIN articles a ON at.target_id = a.id WHERE a.profile_id = #{self.id} AND at.target_type = 'Article' ORDER BY updated_at DESC")
  91 + Scrap.find_by_sql("SELECT id, updated_at, '#{Scrap.to_s}' AS klass FROM #{Scrap.table_name} WHERE scraps.receiver_id = #{self.id} AND scraps.scrap_id IS NULL UNION SELECT id, updated_at, '#{ActionTracker::Record.to_s}' AS klass FROM #{ActionTracker::Record.table_name} WHERE action_tracker.target_id = #{self.id} and action_tracker.verb != 'join_community' and action_tracker.verb != 'leave_scrap' UNION SELECT at.id, at.updated_at, '#{ActionTracker::Record.to_s}' AS klass FROM #{ActionTracker::Record.table_name} at INNER JOIN articles a ON at.target_id = a.id WHERE a.profile_id = #{self.id} AND at.target_type = 'Article' ORDER BY updated_at DESC")
92 92 end
93 93  
94 94 end
... ...
app/models/person.rb
... ... @@ -22,8 +22,6 @@ class Person &lt; Profile
22 22 super
23 23 end
24 24  
25   - acts_as_having_hotspots
26   -
27 25 named_scope :members_of, lambda { |resources|
28 26 resources = [resources] if !resources.kind_of?(Array)
29 27 conditions = resources.map {|resource| "role_assignments.resource_type = '#{resource.class.base_class.name}' AND role_assignments.resource_id = #{resource.id || -1}"}.join(' OR ')
... ... @@ -32,7 +30,7 @@ class Person &lt; Profile
32 30  
33 31 def has_permission_with_plugins?(permission, profile)
34 32 permissions = [has_permission_without_plugins?(permission, profile)]
35   - permissions += enabled_plugins.map do |plugin|
  33 + permissions += plugins.map do |plugin|
36 34 plugin.has_permission?(self, permission, profile)
37 35 end
38 36 permissions.include?(true)
... ... @@ -73,10 +71,7 @@ class Person &lt; Profile
73 71 Friendship.find(:all, :conditions => { :friend_id => person.id}).each { |friendship| friendship.destroy }
74 72 end
75 73  
76   - after_destroy :destroy_user
77   - def destroy_user
78   - self.user.destroy if self.user
79   - end
  74 + belongs_to :user, :dependent => :delete
80 75  
81 76 def can_control_scrap?(scrap)
82 77 begin
... ... @@ -458,7 +453,7 @@ class Person &lt; Profile
458 453 end
459 454  
460 455 def activities
461   - Scrap.find_by_sql("SELECT id, updated_at, '#{Scrap.to_s}' AS klass FROM #{Scrap.table_name} WHERE scraps.receiver_id = #{self.id} AND scraps.scrap_id IS NULL UNION SELECT id, updated_at, '#{ActionTracker::Record.to_s}' AS klass FROM #{ActionTracker::Record.table_name} WHERE action_tracker.user_id = #{self.id} ORDER BY updated_at DESC")
  456 + Scrap.find_by_sql("SELECT id, updated_at, '#{Scrap.to_s}' AS klass FROM #{Scrap.table_name} WHERE scraps.receiver_id = #{self.id} AND scraps.scrap_id IS NULL UNION SELECT id, updated_at, '#{ActionTracker::Record.to_s}' AS klass FROM #{ActionTracker::Record.table_name} WHERE action_tracker.user_id = #{self.id} and action_tracker.verb != 'leave_scrap_to_self' and action_tracker.verb != 'add_member_in_community' ORDER BY updated_at DESC")
462 457 end
463 458  
464 459 protected
... ...
app/models/product.rb
1 1 class Product < ActiveRecord::Base
  2 +
2 3 belongs_to :enterprise
3 4 has_one :region, :through => :enterprise
4 5 validates_presence_of :enterprise
... ... @@ -163,7 +164,7 @@ class Product &lt; ActiveRecord::Base
163 164  
164 165 def total_production_cost
165 166 return inputs_cost if price_details.empty?
166   - inputs_cost + price_details.map(&:price).inject { |sum,price| sum + price }
  167 + inputs_cost + price_details.map(&:price).inject(0){ |sum,price| sum + price }
167 168 end
168 169  
169 170 def price_described?
... ...
app/models/profile.rb
... ... @@ -60,7 +60,8 @@ class Profile &lt; ActiveRecord::Base
60 60 }
61 61  
62 62 acts_as_accessible
63   - acts_as_having_hotspots
  63 +
  64 + include Noosfero::Plugin::HotSpot
64 65  
65 66 named_scope :memberships_of, lambda { |person| { :select => 'DISTINCT profiles.*', :joins => :role_assignments, :conditions => ['role_assignments.accessor_type = ? AND role_assignments.accessor_id = ?', person.class.base_class.name, person.id ] } }
66 67 #FIXME: these will work only if the subclass is already loaded
... ... @@ -69,7 +70,7 @@ class Profile &lt; ActiveRecord::Base
69 70 named_scope :templates, :conditions => {:is_template => true}
70 71  
71 72 def members
72   - scopes = dispatch_scopes(:organization_members, self)
  73 + scopes = plugins.dispatch_scopes(:organization_members, self)
73 74 scopes << Person.members_of(self)
74 75 scopes.size == 1 ? scopes.first : Person.or_scope(scopes)
75 76 end
... ... @@ -113,6 +114,8 @@ class Profile &lt; ActiveRecord::Base
113 114 has_many :scraps_received, :class_name => 'Scrap', :foreign_key => :receiver_id, :order => "updated_at DESC", :dependent => :destroy
114 115 belongs_to :template, :class_name => 'Profile', :foreign_key => 'template_id'
115 116  
  117 + has_many :comments_received, :class_name => 'Comment', :through => :articles, :source => :comments
  118 +
116 119 # FIXME ugly workaround
117 120 def self.human_attribute_name(attrib)
118 121 _(self.superclass.human_attribute_name(attrib))
... ... @@ -255,7 +258,7 @@ class Profile &lt; ActiveRecord::Base
255 258 self.categories(true)
256 259 self.solr_save
257 260 end
258   - self.categories(reload)
  261 + self.categories(reload)
259 262 end
260 263  
261 264 def category_ids=(ids)
... ...
app/models/task.rb
... ... @@ -31,7 +31,7 @@ class Task &lt; ActiveRecord::Base
31 31 end
32 32 end
33 33  
34   - belongs_to :requestor, :class_name => 'Person', :foreign_key => :requestor_id
  34 + belongs_to :requestor, :class_name => 'Profile', :foreign_key => :requestor_id
35 35 belongs_to :target, :foreign_key => :target_id, :polymorphic => true
36 36  
37 37 validates_uniqueness_of :code, :on => :create
... ...
app/models/uploaded_file.rb
... ... @@ -67,7 +67,7 @@ class UploadedFile &lt; Article
67 67 'upload-file'
68 68 end
69 69 end
70   -
  70 +
71 71 def mime_type
72 72 content_type
73 73 end
... ... @@ -129,6 +129,12 @@ class UploadedFile &lt; Article
129 129 end
130 130 end
131 131  
  132 + def extension
  133 + dotindex = self.filename.rindex('.')
  134 + return nil unless dotindex
  135 + self.filename[(dotindex+1)..-1].downcase
  136 + end
  137 +
132 138 def allow_children?
133 139 false
134 140 end
... ... @@ -144,4 +150,5 @@ class UploadedFile &lt; Article
144 150 def uploaded_file?
145 151 true
146 152 end
  153 +
147 154 end
... ...
app/models/user.rb
... ... @@ -30,7 +30,7 @@ class User &lt; ActiveRecord::Base
30 30  
31 31 after_create do |user|
32 32 user.person ||= Person.new
33   - user.person.attributes = user.person_data.merge(:identifier => user.login, :user_id => user.id, :environment_id => user.environment_id)
  33 + user.person.attributes = user.person_data.merge(:identifier => user.login, :user => user, :environment_id => user.environment_id)
34 34 user.person.name ||= user.login
35 35 user.person.visible = false unless user.activated?
36 36 user.person.save!
... ... @@ -88,13 +88,13 @@ class User &lt; ActiveRecord::Base
88 88 attr_protected :activated_at
89 89  
90 90 # Virtual attribute for the unencrypted password
91   - attr_accessor :password
  91 + attr_accessor :password, :name
92 92  
93 93 validates_presence_of :login, :email
94 94 validates_format_of :login, :with => Profile::IDENTIFIER_FORMAT, :if => (lambda {|user| !user.login.blank?})
95 95 validates_presence_of :password, :if => :password_required?
96   - validates_presence_of :password_confirmation, :if => :password_required?, :if => (lambda {|user| !user.password.blank?})
97   - validates_length_of :password, :within => 4..40, :if => :password_required?, :if => (lambda {|user| !user.password.blank?})
  96 + validates_presence_of :password_confirmation, :if => :password_required?
  97 + validates_length_of :password, :within => 4..40, :if => :password_required?
98 98 validates_confirmation_of :password, :if => :password_required?
99 99 validates_length_of :login, :within => 2..40, :if => (lambda {|user| !user.login.blank?})
100 100 validates_length_of :email, :within => 3..100, :if => (lambda {|user| !user.email.blank?})
... ... @@ -228,7 +228,12 @@ class User &lt; ActiveRecord::Base
228 228 end
229 229  
230 230 def name
231   - person ? person.name : login
  231 + name = (self[:name] || login)
  232 + person.nil? ? name : (person.name || name)
  233 + end
  234 +
  235 + def name= name
  236 + self[:name] = name
232 237 end
233 238  
234 239 def enable_email!
... ... @@ -274,6 +279,11 @@ class User &lt; ActiveRecord::Base
274 279 15 # in minutes
275 280 end
276 281  
  282 +
  283 + def not_require_password!
  284 + @is_password_required = false
  285 + end
  286 +
277 287 protected
278 288 # before filter
279 289 def encrypt_password
... ... @@ -282,9 +292,13 @@ class User &lt; ActiveRecord::Base
282 292 self.password_type ||= User.system_encryption_method.to_s
283 293 self.crypted_password = encrypt(password)
284 294 end
285   -
  295 +
286 296 def password_required?
287   - crypted_password.blank? || !password.blank?
  297 + (crypted_password.blank? || !password.blank?) && is_password_required?
  298 + end
  299 +
  300 + def is_password_required?
  301 + @is_password_required.nil? ? true : @is_password_required
288 302 end
289 303  
290 304 def make_activation_code
... ... @@ -292,6 +306,7 @@ class User &lt; ActiveRecord::Base
292 306 end
293 307  
294 308 def deliver_activation_code
  309 + return if person.is_template?
295 310 User::Mailer.deliver_activation_code(self) unless self.activation_code.blank?
296 311 end
297 312  
... ...
app/views/account/login.rhtml
... ... @@ -13,6 +13,8 @@
13 13  
14 14 <%= f.password_field :password %>
15 15  
  16 + <%= @plugins.dispatch(:login_extra_contents).collect { |content| instance_eval(&content) }.join("") %>
  17 +
16 18 <% button_bar do %>
17 19 <%= submit_button( 'login', _('Log in') )%>
18 20 <% if is_thickbox %>
... ... @@ -23,8 +25,13 @@
23 25 <% end %>
24 26  
25 27 <% button_bar do %>
26   - <%= button :add, _("New user"), :controller => 'account', :action => 'signup' %>
27   - <%= button :help, _("I forgot my password!"), :controller => 'account', :action => 'forgot_password' %>
  28 + <% unless @plugins.dispatch(:allow_user_registration).include?(false) %>
  29 + <%= button :add, _("New user"), :controller => 'account', :action => 'signup' %>
  30 + <% end %>
  31 +
  32 + <% unless @plugins.dispatch(:allow_password_recovery).include?(false) %>
  33 + <%= button :help, _("I forgot my password!"), :controller => 'account', :action => 'forgot_password' %>
  34 + <% end %>
28 35 <% end %>
29 36  
30 37 </div><!-- end class="login-box" -->
... ...
app/views/account/login_block.rhtml
... ... @@ -9,25 +9,30 @@
9 9 @user ||= User.new
10 10 %>
11 11  
12   - <% labelled_form_for :user, @user,
13   - :url => login_url do |f| %>
  12 + <% labelled_form_for :user, @user, :url => login_url do |f| %>
14 13  
15   - <%= f.text_field :login, :onchange => 'this.value = convToValidLogin( this.value )' %>
  14 + <%= f.text_field :login, :onchange => 'this.value = convToValidLogin( this.value )' %>
16 15  
17   - <%= f.password_field :password %>
  16 + <%= f.password_field :password %>
  17 +
  18 + <%= @plugins.dispatch(:login_extra_contents).collect { |content| instance_eval(&content) }.join("") %>
18 19  
19 20 <% button_bar do %>
20 21 <%= submit_button( 'login', _('Log in') )%>
21   - <%= link_to content_tag( 'span', _('New user') ),
22   - { :controller => 'account', :action => 'signup' },
23   - :class => 'button with-text icon-add' %>
  22 + <% unless @plugins.dispatch(:allow_user_registration).include?(false) %>
  23 + <%= link_to content_tag( 'span', _('New user') ),
  24 + { :controller => 'account', :action => 'signup' },
  25 + :class => 'button with-text icon-add' %>
  26 + <% end %>
24 27 <% end %>
25 28  
26 29 <% end %>
27 30  
28   - <p class="forgot-passwd">
29   - <%= link_to _("I forgot my password!"), :controller => 'account', :action => 'forgot_password' %>
30   - </p>
  31 + <% unless @plugins.dispatch(:allow_password_recovery).include?(false) %>
  32 + <p class="forgot-passwd">
  33 + <%= link_to _("I forgot my password!"), :controller => 'account', :action => 'forgot_password' %>
  34 + </p>
  35 + <% end %>
31 36  
32 37 </div>
33 38  
... ...
app/views/admin_panel/index.rhtml
1 1 <h1><%= _('Administrator Panel') %></h1>
2 2  
3   -<p><%= _('You, as an environment administrator, has the following options:')%></p>
  3 +<h2><%= _('System settings') %></h2>
4 4  
5 5 <table>
6   - <tr><td><%= link_to _('Edit environment settings'), :action => 'site_info' %></td></tr>
7   - <tr><td><%= link_to __('Edit message for disabled enterprises'), :action => 'message_for_disabled_enterprise' %></td></tr>
8   - <tr><td><%= link_to _('Enable/disable features'), :controller => 'features' %></td></tr>
9   - <tr><td><%= link_to _('Enable/disable plugins'), :controller => 'plugins' %></td></tr>
10   - <tr><td><%= link_to _('Edit sideboxes'), :controller => 'environment_design'%></td></tr>
11   - <tr><td><%= link_to _('Manage Categories'), :controller => 'categories'%></td></tr>
12   - <tr><td><%= link_to _('Manage User roles'), :controller => 'role' %></td></tr>
13   - <tr><td><%= link_to _('Manage users'), :controller => 'users' %></td></tr>
14   - <tr><td><%= link_to _('Manage Validators by region'), :controller => 'region_validators' %></td></tr>
15   - <tr><td><%= link_to _('Edit Templates'), :controller => 'templates' %></td></tr>
16   - <tr><td><%= link_to _('Manage Fields'), :controller => 'features', :action => 'manage_fields' %></td></tr>
17   - <tr><td><%= link_to _('Set Portal'), :action => 'set_portal_community' %></td></tr>
18   - <tr><td><%= link_to _('Manage Licenses'), :controller =>'licenses' %></td></tr>
19   - <% @plugins.dispatch(:admin_panel_links).each do |link| %>
  6 + <tr><td><%= link_to _('Environment settings'), :action => 'site_info' %></td></tr>
  7 + <tr><td><%= link_to _('Features'), :controller => 'features' %></td></tr>
  8 + <tr><td><%= link_to _('Plugins'), :controller => 'plugins' %></td></tr>
  9 + <tr><td><%= link_to _('Sideboxes'), :controller => 'environment_design'%></td></tr>
  10 + <tr><td><%= link_to _('Homepage'), :action => 'set_portal_community' %></td></tr>
  11 + <tr><td><%= link_to _('Licenses'), :controller =>'licenses' %></td></tr>
  12 +</table>
  13 +
  14 +<h2><%= _('Profiles') %></h2>
  15 +
  16 +<table>
  17 + <tr><td><%= link_to _('User roles'), :controller => 'role' %></td></tr>
  18 + <tr><td><%= link_to _('Users'), :controller => 'users' %></td></tr>
  19 + <tr><td><%= link_to _('Profile templates'), :controller => 'templates' %></td></tr>
  20 + <tr><td><%= link_to _('Fields'), :controller => 'features', :action => 'manage_fields' %></td></tr>
  21 +</table>
  22 +
  23 +
  24 +<%
  25 + plugin_links = @plugins.dispatch(:admin_panel_links)
  26 +%>
  27 +<% unless plugin_links.empty? %>
  28 + <h2><%= _('Plugins') %></h2>
  29 + <table>
  30 + <% plugin_links.each do |link| %>
20 31 <tr><td><%= link_to link[:title], link[:url] %></td></tr>
21 32 <% end %>
  33 + </table>
  34 +<% end %>
  35 +
  36 +<h2><%= _('Enterprise-related settings') %></h2>
  37 +
  38 +<table>
  39 + <tr><td><%= link_to __('Message for disabled enterprises'), :action => 'message_for_disabled_enterprise' %></td></tr>
  40 + <tr><td><%= link_to _('Validators by region'), :controller => 'region_validators' %></td></tr>
  41 + <tr><td><%= link_to _('Categories'), :controller => 'categories'%></td></tr>
22 42 </table>
... ...
app/views/box_organizer/_block_types.rhtml 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +<% block_types.in_groups_of(2) do |block1, block2| %>
  2 + <div style='float: left; width: 48%; padding-top: 2px;'>
  3 + <%= labelled_radio_button(block1.description, :type, block1.name) %>
  4 + </div>
  5 + <% if block2 %>
  6 + <div style='float: left; width: 48%; padding-top: 2px;'>
  7 + <%= labelled_radio_button(block2.description, :type, block2.name) %>
  8 + </div>
  9 + <% end %>
  10 +<% end %>
... ...
app/views/box_organizer/_highlights_block.rhtml
1 1 <strong><%= _('Highlights') %></strong>
2   -<div id='edit-highlights-block'>
  2 +<div id='edit-highlights-block' style='width:450px'>
3 3 <table id='highlights' class='noborder'>
4 4 <tr><th><%= _('Image') %></th><th><%= _('Address') %></th><th><%= _('Position') %></th><th><%= _('Title') %></th></tr>
5 5 <% for image in @block.images do %>
... ...
app/views/box_organizer/_link_list_block.rhtml
1 1 <strong><%= _('Links') %></strong>
2   -<div id='edit-link-list-block'>
  2 +<div id='edit-link-list-block' style='width:450px'>
3 3 <table id='links' class='noborder'>
4 4 <tr><th><%= _('Icon') %></th><th><%= _('Name') %></th><th><%= _('Address') %></th></tr>
5 5 <% for link in @block.links do %>
... ...
app/views/box_organizer/_raw_html_block.rhtml
1   -<%= labelled_form_field(_('HTML code'), text_area(:block, :html, :style => 'width: 100%', :rows => 15)) %>
  1 +<%= labelled_form_field(_('HTML code'), text_area(:block, :html, :style => 'width: 90%', :rows => 15)) %>
... ...
app/views/box_organizer/add_block.rhtml
1   -<% form_tag do %>
2   -
3   - <p><%= _('In what area do you want to put your new block?') %></p>
4   -
5   - <%# FIXME hardcoded stuff %>
6   - <%= select_tag('box_id', options_for_select(@boxes.select { |item| item.position != 1 }.map {|item| [ _("Area %d") % item.position, item.id]})) %>
7   -
8   - <p><%= _('Select the type of block you want to add to your page.') %></p>
9   -
10   - <% @block_types.in_groups_of(2) do |block1, block2| %>
11   - <div style='float: left; width: 48%; padding-top: 2px;'>
12   - <%= radio_button_tag('type', block1.name) %>
13   - <%= label_tag "type_#{block1.name.downcase}", block1.description %>
  1 +<div style='height:350px'>
  2 + <% form_tag do %>
  3 +
  4 + <p><%= _('In what area do you want to put your new block?') %></p>
  5 +
  6 + <% @boxes.each do |box| %>
  7 + <%= labelled_radio_button(_("Area %d") % box.position, :box_id, box.id, box.central?, { :class => 'box-position', 'data-position' => box.position }) %>
  8 + <% end %>
  9 +
  10 + <script type="text/javascript">
  11 + (function ($) {
  12 + $(document).ready(function () {
  13 + $(".box-position").live('change', function () {
  14 + if ($(this).attr('data-position') == '1') {
  15 + $('#center-block-types').show();
  16 + $('#side-block-types').hide();
  17 + } else {
  18 + $('#center-block-types').hide();
  19 + $('#side-block-types').show();
  20 + };
  21 + });
  22 + })})(jQuery);
  23 + </script>
  24 +
  25 + <p><%= _('Select the type of block you want to add to your page.') %></p>
  26 +
  27 + <div id='center-block-types'>
  28 + <%= render :partial => 'block_types', :locals => { :block_types => @center_block_types } %>
14 29 </div>
15   - <% if block2 %>
16   - <div style='float: left; width: 48%; padding-top: 2px;'>
17   - <%= radio_button_tag('type', block2.name) %>
18   - <%= label_tag "type_#{block2.name.downcase}", block2.description %>
19   - </div>
  30 +
  31 + <div id='side-block-types' style='display:none'>
  32 + <%= render :partial => 'block_types', :locals => { :block_types => @side_block_types } %>
  33 + </div>
  34 +
  35 + <br style='clear: both'/>
  36 +
  37 + <% button_bar do %>
  38 + <%= submit_button(:add, _("Add")) %>
  39 + <%= colorbox_close_button(_('Close')) %>
20 40 <% end %>
21   - <% end %>
22   - <br style='clear: both'/>
23   -
24   - <% button_bar do %>
25   - <%= submit_button(:add, _("Add")) %>
26   - <%= lightbox_close_button(_('Close')) %>
27   - <% end %>
28 41  
29   -<% end %>
  42 + <% end %>
  43 +</div>
... ...
app/views/box_organizer/edit.rhtml
1   -<h2><%= _('Editing block') %></h2>
  1 +<div style='width: 500px;'>
  2 + <h2><%= _('Editing block') %></h2>
2 3  
3   -<% form_tag(:action => 'save', :id => @block.id) do %>
  4 + <% form_tag(:action => 'save', :id => @block.id) do %>
4 5  
5   - <%= labelled_form_field(_('Custom title for this block: '), text_field(:block, :title, :maxlength => 20)) %>
  6 + <%= labelled_form_field(_('Custom title for this block: '), text_field(:block, :title, :maxlength => 20)) %>
6 7  
7   - <%= render :partial => partial_for_class(@block.class) %>
  8 + <%= render :partial => partial_for_class(@block.class) %>
8 9  
9   - <%= labelled_form_field _('Display this block:'), '' %>
10   - <div style='margin-left: 10px'>
11   - <%= radio_button(:block, :display, 'always') %>
12   - <%= label_tag('block_display_always', _('In all pages')) %>
13   - <br/>
14   - <%= radio_button(:block, :display, 'home_page_only') %>
15   - <%= label_tag('block_display_home_page_only', _('Only in the homepage')) %>
16   - <br/>
17   - <%= radio_button(:block, :display, 'except_home_page') %>
18   - <%= label_tag('block_display_except_home_page', _('In all pages, except in the homepage')) %>
19   - <br/>
20   - <%= radio_button(:block, :display, 'never') %>
21   - <%= label_tag('block_display_never', _("Don't display")) %>
22   - </div>
  10 + <%= labelled_form_field _('Display this block:'), '' %>
  11 + <div style='margin-left: 10px'>
  12 + <%= radio_button(:block, :display, 'always') %>
  13 + <%= label_tag('block_display_always', _('In all pages')) %>
  14 + <br/>
  15 + <%= radio_button(:block, :display, 'home_page_only') %>
  16 + <%= label_tag('block_display_home_page_only', _('Only in the homepage')) %>
  17 + <br/>
  18 + <%= radio_button(:block, :display, 'except_home_page') %>
  19 + <%= label_tag('block_display_except_home_page', _('In all pages, except in the homepage')) %>
  20 + <br/>
  21 + <%= radio_button(:block, :display, 'never') %>
  22 + <%= label_tag('block_display_never', _("Don't display")) %>
  23 + </div>
23 24  
24 25 <%= labelled_form_field(_('Show for:'), select(:block, :language, [ [ _('all languages'), 'all']] + environment.locales.map {|key, value| [value, key]} )) %>
25 26  
26   - <% button_bar do %>
27   - <%= submit_button(:save, _('Save')) %>
28   - <%= lightbox_close_button(_('Cancel')) %>
29   - <% end %>
  27 + <% button_bar do %>
  28 + <%= submit_button(:save, _('Save')) %>
  29 + <%= colorbox_close_button(_('Cancel')) %>
  30 + <% end %>
30 31  
31   -<% end %>
  32 + <% end %>
  33 +</div>
... ...
app/views/box_organizer/index.rhtml
1 1 <h1><%= _('Editing sideboxes')%></h1>
2 2  
3 3 <% button_bar do %>
4   - <%= lightbox_button('add', _('Add a block'), { :action => 'add_block' }) %>
  4 + <%= colorbox_button('add', _('Add a block'), { :action => 'add_block' }) %>
5 5 <%= button(:back, _('Back to control panel'), :controller => (profile.nil? ? 'admin_panel': 'profile_editor')) %>
6 6 <% end %>
... ...
app/views/cms/view.rhtml
... ... @@ -49,13 +49,13 @@
49 49 <%= article.class.short_description %>
50 50 </td>
51 51 <td class="article-controls">
52   - <%= button_without_text :edit, _('Edit'), :action => 'edit', :id => article.id %>
  52 + <%= expirable_button article, :edit, _('Edit'), {:action => 'edit', :id => article.id} if !remove_content_button(:edit) %>
53 53 <%= button_without_text :eyes, _('Public view'), article.view_url %>
54   - <%= display_spread_button(profile, article) unless article.folder? %>
55   - <% if !environment.enabled?('cant_change_homepage') %>
56   - <%= button_without_text :home, _('Use as homepage'), { :action => 'set_home_page', :id => article.id }, :method => :post %>
  54 + <%= display_spread_button(profile, article) unless article.folder? || remove_content_button(:spread)%>
  55 + <% if !environment.enabled?('cant_change_homepage') && !remove_content_button(:home) %>
  56 + <%= expirable_button article, :home, _('Use as homepage'), { :action => 'set_home_page', :id => article.id }, :method => :post %>
57 57 <% end %>
58   - <%= display_delete_button(article) %>
  58 + <%= display_delete_button(article) if !remove_content_button(:delete) %>
59 59 </td>
60 60 </tr>
61 61 <% end %>
... ...
app/views/content_viewer/_article_toolbar.rhtml
1 1 <div<%= user && " class='logged-in'" %>>
2 2 <div id="article-actions">
3 3  
4   - <% if @page.allow_edit?(user) %>
5   - <%= link_to content_tag( 'span', label_for_edit_article(@page) ),
6   - profile.admin_url.merge({ :controller => 'cms', :action => 'edit', :id => @page.id }),
7   - :class => 'button with-text icon-edit' %>
  4 +
  5 + <% if @page.allow_edit?(user) && !remove_content_button(:edit) %>
  6 + <% content = content_tag('span', label_for_edit_article(@page)) %>
  7 + <% url = profile.admin_url.merge({ :controller => 'cms', :action => 'edit', :id => @page.id }) %>
  8 + <%= expirable_button @page, :edit, content, url %>
8 9 <% end %>
9 10  
10   - <% if @page != profile.home_page && !@page.has_posts? && @page.allow_delete?(user) %>
11   - <%= link_to content_tag( 'span', _('Delete') ),
12   - profile.admin_url.merge({ :controller => 'cms', :action => 'destroy', :id => @page}),
13   - :method => :post,
14   - :class => 'button with-text icon-delete',
15   - :confirm => delete_article_message(@page) %>
  11 + <% if @page != profile.home_page && !@page.has_posts? && @page.allow_delete?(user) && !remove_content_button(:delete)%>
  12 + <% content = content_tag( 'span', _('Delete') ) %>
  13 + <% url = profile.admin_url.merge({ :controller => 'cms', :action => 'destroy', :id => @page}) %>
  14 + <% options = {:method => :post, :confirm => delete_article_message(@page)} %>
  15 + <%= expirable_button @page, :delete, content, url, options %>
16 16 <% end %>
17 17  
18   - <% if !@page.folder? && @page.allow_spread?(user) %>
  18 + <% if !@page.folder? && @page.allow_spread?(user) && !remove_content_button(:spread) %>
  19 + <% content = content_tag( 'span', _('Spread this') ) %>
  20 + <% url = nil %>
19 21 <% if profile.kind_of?(Person) %>
20   - <%= link_to content_tag( 'span', _('Spread this') ),
21   - profile.admin_url.merge({ :controller => 'cms', :action => 'publish', :id => @page }),
22   - :class => 'button with-text icon-spread' %>
  22 + <% url = profile.admin_url.merge({ :controller => 'cms', :action => 'publish', :id => @page }) %>
23 23 <% elsif profile.kind_of?(Community) && environment.portal_community %>
24   - <%= link_to content_tag( 'span', _('Spread this') ),
25   - profile.admin_url.merge({ :controller => 'cms', :action => 'publish_on_portal_community', :id => @page }),
26   - :class => 'button with-text icon-spread' %>
  24 + <% url = profile.admin_url.merge({ :controller => 'cms', :action => 'publish_on_portal_community', :id => @page }) %>
27 25 <% end %>
  26 + <%= expirable_button @page, :spread, content, url if url %>
28 27 <% end %>
29 28  
30 29 <% if !@page.gallery? && @page.allow_create?(user) %>
31   - <%= link_to _('Add translation'),
32   - profile.admin_url.merge(:controller => 'cms', :action => 'new',
33   - :parent_id => (@page.folder? ? @page : (@page.parent.nil? ? nil : @page.parent)),
34   - :type => @page.type, :article => { :translation_of_id => @page.native_translation.id }),
35   - :class => 'button with-text icon-locale' if @page.translatable? && !@page.native_translation.language.blank? %>
  30 + <% if @page.translatable? && !@page.native_translation.language.blank? && !remove_content_button(:locale) %>
  31 + <% content = _('Add translation') %>
  32 + <% parent_id = (@page.folder? ? @page : (@page.parent.nil? ? nil : @page.parent)) %>
  33 + <% url = profile.admin_url.merge(:controller => 'cms', :action => 'new', :parent_id => parent_id, :type => @page.type, :article => { :translation_of_id => @page.native_translation.id })%>
  34 + <%= expirable_button @page, :locale, content, url %>
  35 + <% end %>
  36 +
36 37 <%= colorbox_button(:new, label_for_new_article(@page), profile.admin_url.merge(:controller => 'cms', :action => 'new', :parent_id => (@page.folder? ? @page : (@page.parent.nil? ? nil : @page.parent)))) %>
37 38 <% end %>
38 39  
... ... @@ -40,8 +41,11 @@
40 41 <%= button('upload-file', _('Upload files'), profile.admin_url.merge(:controller => 'cms', :action => 'upload_files', :parent_id => (@page.folder? ? @page : @page.parent))) %>
41 42 <% end %>
42 43  
43   - <% if !@page.allow_create?(user) && profile.community? && (@page.blog? || @page.parent && @page.parent.blog?) %>
44   - <%= link_to content_tag( 'span', _('Suggest an article') ), profile.admin_url.merge({ :controller => 'cms', :action => 'suggest_an_article'}), :id => 'suggest-article-link', :class => 'button with-text icon-new' %>
  44 + <% if !@page.allow_create?(user) && profile.community? && (@page.blog? || @page.parent && @page.parent.blog?) && !remove_content_button(:suggest) %>
  45 + <% content = content_tag( 'span', _('Suggest an article') ) %>
  46 + <% url = profile.admin_url.merge({ :controller => 'cms', :action => 'suggest_an_article'}) %>
  47 + <% options = {:id => 'suggest-article-link'} %>
  48 + <%= expirable_button @page, :suggest, content, url, options %>
45 49 <% end %>
46 50  
47 51 <%= report_abuse(profile, :link, @page) %>
... ...
app/views/content_viewer/_comment.rhtml
1 1 <li id="<%= comment.anchor %>" class="article-comment">
2 2 <div class="article-comment-inner">
3 3  
4   - <div class="comment-content comment-logged-<%= comment.author ? 'in' : 'out' %> <%= 'comment-from-owner' if ( comment.author && (@page.profile.name == comment.author.name) ) %>">
  4 + <div class="comment-content comment-logged-<%= comment.author ? 'in' : 'out' %> <%= 'comment-from-owner' if ( comment.author && (profile == comment.author) ) %>">
5 5  
6 6 <% if comment.author %>
7 7 <%= link_to image_tag(profile_icon(comment.author, :minor)) +
... ... @@ -29,17 +29,12 @@
29 29 <% end %>
30 30  
31 31 <% comment_balloon do %>
32   - <% if logged_in? && (user == @page.profile || user == comment.author || user.has_permission?(:moderate_comments, @page.profile)) %>
33   - <% button_bar(:style => 'float: right; margin-top: 0px;') do %>
34   - <%= icon_button(:delete, _('Remove this comment and all its replies'), { :profile => params[:profile], :remove_comment => comment.id, :view => params[:view] }, :method => :post, :confirm => _('Are you sure you want to remove this comment and all its replies?')) %>
35   - <% end %>
36   - <% end %>
37 32  
38 33 <div class="comment-details">
39 34 <div class="comment-created-at">
40 35 <%= show_time(comment.created_at) %>
41 36 </div>
42   - <h4><%= comment.title %></h4>
  37 + <h4><%= comment.title.blank? && '&nbsp;' || comment.title %></h4>
43 38 <div class="comment-text">
44 39 <p/>
45 40 <%= txt2html comment.body %>
... ... @@ -57,18 +52,42 @@
57 52 </script>
58 53 <% end %>
59 54 <%= report_abuse(comment.author, :comment_link, comment) if comment.author %>
60   - <%= link_to_function _('Reply'),
61   - "var f = add_comment_reply_form(this, %s); f.find('input[name=comment[title]], textarea').val(''); return false" % comment.id,
  55 +
  56 + <% if comment.spam? %>
  57 + &nbsp;
  58 + <%= link_to_function(_('Mark as NOT SPAM'), 'remove_comment(this, %s); return false;' % url_for(:mark_comment_as_ham => comment.id).to_json, :class => 'comment-footer comment-footer-link comment-footer-hide') %>
  59 + <% else %>
  60 + <% if (logged_in? && (user == profile || user.has_permission?(:moderate_comments, profile))) %>
  61 + &nbsp;
  62 + <%= link_to_function(_('Mark as SPAM'), 'remove_comment(this, %s, %s); return false;' % [url_for(:mark_comment_as_spam => comment.id).to_json, _('Are you sure you want to mark this comment as SPAM?').to_json], :class => 'comment-footer comment-footer-link comment-footer-hide') %>
  63 + <% end %>
  64 + <% end %>
  65 +
  66 + <% if comment.author && comment.author == user %>
  67 + &nbsp;
  68 + <%= expirable_comment_link comment, :edit, _('Edit'), {:action => 'edit_comment', :id => comment.id, :profile => profile.identifier} %>
  69 + <% end %>
  70 +
  71 + <% if logged_in? && (user == profile || user == comment.author || user.has_permission?(:moderate_comments, profile)) %>
  72 + &nbsp;
  73 + <%= link_to_function(_('Remove'), 'remove_comment(this, %s, %s); return false ;' % [url_for(:profile => params[:profile], :remove_comment => comment.id, :view => params[:view]).to_json, _('Are you sure you want to remove this comment and all its replies?').to_json], :class => 'comment-footer comment-footer-link comment-footer-hide remove-children') %>
  74 + <% end %>
  75 +
  76 + <% unless comment.spam? %>
  77 + &nbsp;
  78 + <%= link_to_function _('Reply'),
  79 + "var f = add_comment_reply_form(this, %s); f.find('comment_title, textarea').val(''); return false" % comment.id,
62 80 :class => 'comment-footer comment-footer-link comment-footer-hide',
63 81 :id => 'comment-reply-to-' + comment.id.to_s
64   - %>
  82 + %>
  83 + <% end %>
65 84 </div>
66 85  
67 86 <% end %>
68 87  
69 88 </div>
70 89  
71   - <% unless comment.replies.blank? %>
  90 + <% unless comment.replies.blank? || comment.spam? %>
72 91 <ul class="comment-replies">
73 92 <% comment.replies.each do |reply| %>
74 93 <%= render :partial => 'comment', :locals => { :comment => reply } %>
... ...
app/views/content_viewer/_comment_form.rhtml
... ... @@ -32,15 +32,17 @@ function submit_comment_form(button) {
32 32  
33 33 <div class="post_comment_box <%= @form_div %>">
34 34  
35   -<h4 onclick="var d = jQuery(this).parent('.post_comment_box');
36   - if (d.hasClass('closed')) {
37   - d.removeClass('closed');
38   - d.addClass('opened');
39   - d.find('input[name=comment[title]], textarea').val('');
40   - d.find('.comment_form input[name=comment[<%= focus_on %>]]').focus();
41   - }">
42   - <%= content_tag('a', '', :name => 'comment_form') + _('Post a comment') %>
43   -</h4>
  35 +<% if display_link %>
  36 + <h4 onclick="var d = jQuery(this).parent('.post_comment_box');
  37 + if (d.hasClass('closed')) {
  38 + d.removeClass('closed');
  39 + d.addClass('opened');
  40 + d.find('input[name=comment[title]], textarea').val('');
  41 + d.find('.comment_form input[name=comment[<%= focus_on %>]]').focus();
  42 + }">
  43 + <%= content_tag('a', '', :name => 'comment_form') + _('Post a comment') %>
  44 + </h4>
  45 +<% end %>
44 46  
45 47 <% unless pass_without_comment_captcha? %>
46 48 <div id="recaptcha-container" style="display: none">
... ... @@ -59,7 +61,7 @@ function submit_comment_form(button) {
59 61 </script>
60 62 <% end %>
61 63  
62   -<% form_tag( url_for(@page.view_url.merge({:only_path => true})), { :class => 'comment_form' } ) do %>
  64 +<% form_tag( url, { :class => 'comment_form' } ) do %>
63 65 <%= hidden_field_tag(:confirm, 'false') %>
64 66  
65 67 <%= required_fields_message %>
... ... @@ -84,7 +86,11 @@ function submit_comment_form(button) {
84 86  
85 87 <% button_bar do %>
86 88 <%= submit_button('add', _('Post comment'), :onclick => "submit_comment_form(this); return false") %>
87   - <%= button_to_function :cancel, _('Cancel'), "f=jQuery(this).parents('.post_comment_box'); f.removeClass('opened'); f.addClass('closed'); return false" %>
  89 + <% if cancel_triggers_hide %>
  90 + <%= button_to_function :cancel, _('Cancel'), "f=jQuery(this).parents('.post_comment_box'); f.removeClass('opened'); f.addClass('closed'); return false" %>
  91 + <% else %>
  92 + <%= button('cancel', _('Cancel'), {:action => 'view_page', :profile => profile.identifier, :page => @comment.article.explode_path})%>
  93 + <% end %>
88 94 <% end %>
89 95 <% end %>
90 96  
... ...
app/views/content_viewer/edit_comment.html.erb 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +<h1><%= _('Edit comment') %></h1>
  2 +
  3 +<%= render :partial => 'comment_form', :locals => {:url => {:action => 'edit_comment', :profile => profile.identifier}, :display_link => false, :cancel_triggers_hide => false} %>
... ...
app/views/content_viewer/view_page.rhtml
... ... @@ -98,13 +98,7 @@
98 98 </ul>
99 99  
100 100 <% if @page.accept_comments? %>
101   - <div id="page-comment-form"><%= render :partial => 'comment_form' %></div>
102   - <script type="text/javascript">
103   - jQuery( function() {
104   - jQuery('.article-comment').live('mouseover', function() { jQuery(this).find('.icon-delete:first').show(); });
105   - jQuery('.article-comment').live('mouseout', function() { jQuery(this).find('.icon-delete').hide(); });
106   - });
107   - </script>
  101 + <div id="page-comment-form"><%= render :partial => 'comment_form', :locals => {:url => url_for(@page.view_url.merge({:only_path => true})), :display_link => true, :cancel_triggers_hide => true}%></div>
108 102 <% end %>
109 103 </div><!-- end class="comments" -->
110 104  
... ...
app/views/friends/index.rhtml
... ... @@ -31,7 +31,7 @@
31 31 :class => 'button icon-remove',
32 32 :title => _('remove') %>
33 33 <%= link_to content_tag('span',_('contact')),
34   - friend.url.merge(:controller => 'contact', :action => 'new'),
  34 + friend.url.merge(:controller => 'contact', :action => 'new', :profile => friend.identifier),
35 35 :class => 'button icon-menu-mail',
36 36 :title => _('contact') %>
37 37 </div><!-- end class="controll" -->
... ...
app/views/layouts/_javascript.rhtml
... ... @@ -2,7 +2,8 @@
2 2 'jquery.noconflict.js', 'jquery.cycle.all.min.js', 'thickbox.js', 'lightbox', 'colorbox',
3 3 'jquery-ui-1.8.2.custom.min', 'jquery.scrollTo', 'jquery.form.js', 'jquery-validation/jquery.validate',
4 4 'jquery.cookie', 'jquery.ba-bbq.min.js', 'reflection', 'jquery.tokeninput',
5   -'add-and-join', 'report-abuse', 'catalog', 'manage-products', :cache => 'cache-general' %>
  5 +'add-and-join', 'report-abuse', 'catalog', 'manage-products',
  6 +'jquery-ui-timepicker-addon', :cache => 'cache-general' %>
6 7  
7 8 <% language = FastGettext.locale %>
8 9 <% %w{messages methods}.each do |type| %>
... ...
app/views/layouts/application-ng.rhtml
... ... @@ -56,10 +56,18 @@
56 56 <%= usermenu_logged_in %>
57 57 </span>
58 58 <span class='not-logged-in' style='display: none'>
59   - <%= _("<span class='login'>%s</span> <span class='or'>or</span> <span class='signup'>%s</span>") % [thickbox_inline_popup_link('<i class="icon-menu-login"></i><strong>' + _('Login') + '</strong>', login_url, 'inlineLoginBox', :id => 'link_login'), link_to('<strong>' + _('Sign up') + '</strong>', :controller => 'account', :action => 'signup') ] %>
  59 +
  60 + <%= _("<span class='login'>%s</span>") % thickbox_inline_popup_link('<i class="icon-menu-login"></i><strong>' + _('Login') + '</strong>', login_url, 'inlineLoginBox', :id => 'link_login') %>
  61 + <%= @plugins.dispatch(:alternative_authentication_link).collect { |content| instance_eval(&content) }.join("") %>
  62 +
60 63 <div id='inlineLoginBox' style='display: none;'>
61 64 <%= render :file => 'account/login', :locals => { :is_thickbox => true } %>
62 65 </div>
  66 +
  67 + <% unless @plugins.dispatch(:allow_user_registration).include?(false) %>
  68 + <%= _("<span class='or'>or</span> <span class='signup'>%s</span>") % link_to('<strong>' + _('Sign up') + '</strong>', :controller => 'account', :action => 'signup')%>
  69 + <% end %>
  70 +
63 71 </span>
64 72 <form action="/search" class="search_form" method="get" class="clean">
65 73 <input name="query" size="15" title="<%=_('Search...')%>" onfocus="this.form.className='focused';" onblur="this.form.className=''" />
... ...
app/views/plugins/index.rhtml
1 1 <h1><%= _('Manage plugins') %></h1>
2   -<%= _('Here you can enable or disable any plugin of your environment.')%>
  2 +
  3 +<p>
  4 +<%= _('Select which plugins you want to enable in your environment') %>
  5 +</p>
3 6  
4 7 <% labelled_form_for(:environment, @environment, :url => {:action => 'update'}) do |f| %>
5 8  
6   -<table>
7   - <tr>
8   - <th><%= _('Plugin') %></th>
9   - <th><%= _('Description') %></th>
10   - <th><%= _('Enabled?') %></th>
11   - </tr>
12   - <%= hidden_field_tag('environment[enabled_plugins][]', '') %>
13   - <% @active_plugins.each do |plugin| %>
14   - <tr>
15   - <td><%= plugin.has_admin_url? ? link_to(plugin.plugin_name, plugin.admin_url) : plugin.plugin_name %></td>
16   - <td><%= plugin.plugin_description %></td>
17   - <td><%= check_box_tag "environment[enabled_plugins][]", plugin, @environment.enabled_plugins.include?(plugin.to_s), :id => plugin.plugin_name %></td>
18   - </tr>
19   - <% end %>
20   -</table>
  9 + <table>
  10 + <% @active_plugins.sort_by(&:plugin_name).each do |plugin| %>
  11 + <tr>
  12 + <td style='vertical-align: top'><%= check_box_tag "environment[enabled_plugins][]", plugin, @environment.enabled_plugins.include?(plugin.to_s), :id => plugin.plugin_name %></td>
  13 + <td>
  14 + <%= hidden_field_tag('environment[enabled_plugins][]', '') %>
  15 + <strong><%= plugin.plugin_name %></strong>
  16 + <br/>
  17 + <%= plugin.plugin_description %>
  18 + <% if plugin.has_admin_url? %>
  19 + <br/>
  20 + <br/>
  21 + <%= link_to(_('Configuration'), plugin.admin_url) %>
  22 + <% end %>
  23 + </td>
  24 + </tr>
  25 + <% end %>
  26 + </table>
21 27  
22 28 <div>
23 29 <% button_bar do %>
... ...
app/views/profile/_comment.rhtml
... ... @@ -5,19 +5,35 @@
5 5 <li class="article-comment" style='border-bottom:none;'>
6 6 <div class="article-comment-inner">
7 7  
8   - <div class="comment-content comment-logged-in">
  8 + <div class="comment-content comment-logged-<%= comment.author ? 'in' : 'out' %>">
9 9  
10 10 <% if comment.author %>
11 11 <%= link_to image_tag(profile_icon(comment.author, :minor)),
12   - Person.find(comment.author_id).url,
  12 + comment.author_url,
13 13 :class => 'comment-picture',
14 14 :title => comment.author_name
15 15 %>
  16 + <% else %>
  17 + <% url_image, status_class = comment.author_id ?
  18 + [comment.removed_user_image, 'icon-user-removed'] :
  19 + [str_gravatar_url_for( comment.email ), 'icon-user-unknown'] %>
  20 +
  21 + <%= link_to(
  22 + image_tag(url_image, :onerror=>'gravatarCommentFailback(this)',
  23 + 'data-gravatar'=>str_gravatar_url_for(comment.email)) +
  24 + content_tag('span', comment.author_name, :class => 'comment-info') +
  25 + content_tag('span', comment.message,
  26 + :class => 'comment-user-status comment-user-status-wall ' + status_class),
  27 + gravatar_profile_url(comment.email),
  28 + :target => '_blank',
  29 + :class => 'comment-picture',
  30 + :title => '%s %s' % [comment.author_name, comment.message]
  31 + )%>
16 32 <% end %>
17 33  
18 34 <div class="comment-details">
19 35 <div class="comment-text">
20   - <%= link_to(comment.author_name, comment.author.url) %>
  36 + <%= comment.author.present? ? link_to(comment.author_name, comment.author.url) : content_tag('strong', comment.author_name) %>
21 37 <% unless comment.title.blank? %>
22 38 <span class="comment-title"><%= comment.title %></span><br/>
23 39 <% end %>
... ... @@ -30,7 +46,7 @@
30 46  
31 47 <% if logged_in? && (user == profile || user == comment.author || user.has_permission?(:moderate_comments, profile)) %>
32 48 <% button_bar(:style => 'float: right; margin-top: 0px;') do %>
33   - <%= icon_button(:delete, _('Remove'), { :action => :remove_comment, :comment_id => comment.id }, :method => :get, :confirm => _('Are you sure you want to remove this comment and all its replies?')) %>
  49 + <%= link_to_function(_('Remove'), 'remove_item_wall(this, %s, %s, %s); return false ;' % ["'.article-comment'", url_for(:profile => params[:profile], :action => :remove_comment, :comment_id => comment.id, :view => params[:view]).to_json, _('Are you sure you want to remove this comment and all its replies?').to_json], :class => 'button icon-button icon-delete') %>
34 50 <% end %>
35 51 <% end %>
36 52 <br style="clear: both;" />
... ... @@ -46,6 +62,10 @@
46 62 </script>
47 63 <% end %>
48 64 <%= report_abuse(comment.author, :comment_link, comment) if comment.author %>
  65 + <% if comment.author && comment.author == user %>
  66 + <%= expirable_comment_link comment, :edit, _('Edit'), {:action => 'edit_comment', :id => comment.id, :profile => profile.identifier} %>
  67 + <%= content_tag('span', ' | ', :class => 'comment-footer comment-footer-hide') %>
  68 + <% end %>
49 69 <%= link_to_function _('Reply'),
50 70 "var f = add_comment_reply_form(this, %s); f.find('input[name=comment[title]], textarea').val(''); return false" % comment.id,
51 71 :class => 'comment-footer comment-footer-link comment-footer-hide',
... ...
app/views/profile/_create_article.rhtml
... ... @@ -15,7 +15,7 @@
15 15 <p class='profile-activity-time'><%= time_ago_as_sentence(activity.created_at) %></p>
16 16 <div class='profile-wall-actions'>
17 17 <%= link_to s_('profile|Comment'), '#', { :class => 'focus-on-comment'} %>
18   - <%= link_to_remote(content_tag(:span, _('Remove')), :url =>{:action => 'remove_activity', :activity_id => activity.id, :only_hide => true}, :confirm => _('Are you sure?'), :update => "profile-activity-item-#{activity.id}") if logged_in? && current_person == @profile %>
  18 + <%= link_to_function(_('Remove'), 'remove_item_wall(this, %s, %s, %s); return false ;' % ["'.profile-activity-item'", url_for(:profile => params[:profile], :action => :remove_activity, :activity_id => activity.id, :only_hide => true, :view => params[:view]).to_json, _('Are you sure you want to remove this activity and all its replies?').to_json]) if logged_in? && current_person == @profile %>
19 19 </div>
20 20 </div>
21 21  
... ...
app/views/profile/_default_activity.rhtml
... ... @@ -6,7 +6,7 @@
6 6 <p class='profile-activity-time'><%= time_ago_as_sentence(activity.created_at) %></p>
7 7 <div class='profile-wall-actions'>
8 8 <%= link_to s_('profile|Comment'), '#', { :class => 'focus-on-comment'} %>
9   - <%= link_to_remote(content_tag(:span, _('Remove')), :confirm => _('Are you sure?'), :url =>{:action => 'remove_activity', :activity_id => activity.id}, :update => "profile-activity-item-#{activity.id}") if logged_in? && current_person == @profile %>
  9 + <%= link_to_function(_('Remove'), 'remove_item_wall(this, %s, %s, %s); return false ;' % ["'.profile-activity-item'", url_for(:profile => params[:profile], :action => :remove_activity, :activity_id => activity.id, :view => params[:view]).to_json, _('Are you sure you want to remove this activity and all its replies?').to_json]) if logged_in? && current_person == @profile %>
10 10 </div>
11 11 </div>
12 12  
... ...
app/views/profile/_leave_scrap.rhtml
... ... @@ -5,7 +5,7 @@
5 5 <p class='profile-activity-text'><%= link_to activity.user.name, activity.user.url %> <%= describe activity %></p>
6 6 <p class='profile-activity-time'><%= time_ago_as_sentence(activity.created_at) %></p>
7 7 <div class='profile-wall-actions'>
8   - <%= link_to_remote(content_tag(:span, _('Remove')), :confirm => _('Are you sure?'), :url =>{:action => 'remove_activity', :activity_id => activity.id}, :update => "profile-activity-item-#{activity.id}") if logged_in? && current_person == @profile %>
  8 + <%= link_to_function(_('Remove'), 'remove_item_wall(this, %s, %s, %s); return false ;' % ["'.profile-activity-item'", url_for(:profile => params[:profile], :action => :remove_activity, :activity_id => activity.id, :view => params[:view]).to_json, _('Are you sure you want to remove this activity and all its replies?').to_json]) if logged_in? && current_person == @profile %>
9 9 </div>
10 10 </div>
11 11  
... ...
app/views/profile/_profile_scrap.rhtml
... ... @@ -12,7 +12,7 @@
12 12 <%= link_to_function s_('profile|Comment'), "hide_and_show(['#profile-wall-message-response-#{scrap.id}'],['#profile-wall-reply-#{scrap.id}', '#profile-wall-reply-form-#{scrap.id}']);$('reply_content_#{scrap.id}').value='';$('reply_content_#{scrap.id}').focus();return false", :class => "profile-send-reply" %>
13 13 </span>
14 14 <% end %>
15   - <%= link_to_remote(content_tag(:span, _('Remove')), :confirm => _('Are you sure?'), :url =>{:action => 'remove_scrap', :scrap_id => scrap.id}, :update => "profile-activity-item-#{scrap.id}") if logged_in? && user.can_control_scrap?(scrap) %>
  15 + <%= link_to_function(_('Remove'), 'remove_item_wall(this, %s, %s, %s); return false ;' % ["'.profile-activity-item'", url_for(:profile => params[:profile], :action => :remove_scrap, :scrap_id => scrap.id, :view => params[:view]).to_json, _('Are you sure you want to remove this scrap and all its replies?').to_json]) if logged_in? && user.can_control_scrap?(scrap) %>
16 16 </div>
17 17 </div>
18 18  
... ...
app/views/profile/_upload_image.rhtml
... ... @@ -6,7 +6,7 @@
6 6 <p class='profile-activity-text'><%= link_to activity.user.name, activity.user.url %> <%= describe activity %></p>
7 7 <p class='profile-activity-time'><%= time_ago_as_sentence(activity.created_at) %></p>
8 8 <div class='profile-wall-actions'>
9   - <%= link_to_remote(content_tag(:span, _('Remove')), :confirm => _('Are you sure?'), :url =>{:action => 'remove_activity', :activity_id => activity.id}, :update => "profile-activity-item-#{activity.id}") if logged_in? && current_person == @profile %>
  9 + <%= link_to_function(_('Remove'), 'remove_item_wall(this, %s, %s, %s); return false ;' % ["'.profile-activity-item'", url_for(:profile => params[:profile], :action => :remove_activity, :activity_id => activity.id, :view => params[:view]).to_json, _('Are you sure you want to remove this activity and all its replies?').to_json]) if logged_in? && current_person == @profile %>
10 10 </div>
11 11 </div>
12 12 </div>
... ...
app/views/profile_editor/index.rhtml
... ... @@ -66,6 +66,8 @@
66 66  
67 67 <%= control_panel_button(_('Manage my groups'), 'groups', :controller => 'memberships') if profile.person? %>
68 68  
  69 + <%= control_panel_button(_('Manage SPAM'), 'manage-spam', :controller => 'spam', :action => 'index') %>
  70 +
69 71 <% @plugins.dispatch(:control_panel_buttons).each do |button| %>
70 72 <%= control_panel_button(button[:title], button[:icon], button[:url]) %>
71 73 <% end %>
... ...
app/views/search/_display_results.rhtml
1   -<div id="search-results" class="<%= @results.size == 1 ? 'only-one-result-box' : 'multiple-results-boxes' %>">
  1 +<div id="search-results" class="<%= !multiple_search? ? 'only-one-result-box' : 'multiple-results-boxes' %>">
2 2 <% @order.each do |name| %>
3 3 <% results = @results[name] %>
4 4 <% empty = results.nil? || results.empty? %>
... ... @@ -7,7 +7,7 @@
7 7 <% if not empty %>
8 8 <% partial = partial_for_class(results.first.class.class_name.constantize) %>
9 9  
10   - <% if @results.size > 1 %>
  10 + <% if multiple_search? %>
11 11 <h3><%= @names[name] %></h3>
12 12 <% if results.total_entries > SearchController::MULTIPLE_SEARCH_LIMIT %>
13 13 <%= link_to(_('see all (%d)') % results.total_entries, params.merge(:action => name), :class => 'see-more' ) %>
... ... @@ -22,9 +22,10 @@
22 22 </ul>
23 23 </div>
24 24 <% else %>
25   - <% if @results.size > 1 %>
  25 + <% if multiple_search? %>
26 26 <h3><%= @names[name] %></h3>
27 27 <% end %>
  28 +
28 29 <div class="search-results-innerbox search-results-type-empty">
29 30 <div> <%= _('None') %> </div>
30 31 </div>
... ...
app/views/search/_image.rhtml
1 1 <div class="search-image-container">
2 2  
3 3 <% if image.is_a? UploadedFile and image.filename %>
4   - <% extension = image.filename[(image.filename.rindex('.')+1)..-1].downcase %>
  4 + <% extension = image.extension %>
5 5 <% if ['jpg', 'jpeg', 'gif', 'png', 'tiff', 'svg'].include? extension %>
6 6 <%= link_to '', image.view_url, :class => "search-image-pic", :style => 'background-image: url(%s)'% image.public_filename(:thumb) %>
7 7 <% if image.width && image.height %>
... ...
app/views/search/_profile.rhtml
1 1 <li class="search-profile-item">
2   -<% if @empty_query or @results.size > 1 or !profile.enterprise? %>
  2 +<% if @empty_query or multiple_search? or !profile.enterprise? %>
3 3 <%= profile_image_link profile, :portrait, 'div',
4 4 @filter == 'more_recent' ? profile.send(@filter + '_label') + show_date(profile.created_at) : profile.send(@filter + '_label') %>
5 5 <% else %>
... ...
app/views/search/communities.rhtml
... ... @@ -4,7 +4,7 @@
4 4 <% if logged_in? %>
5 5 <% button_bar do %>
6 6 <%# FIXME shouldn't the user create the community in the current environment instead of going to its home environment? %>
7   - <%= button(:add, __('New community'), user.url.merge(:controller => 'memberships', :action => 'new_community')) %>
  7 + <%= button(:add, __('New community'), user.url.merge(:controller => 'memberships', :action => 'new_community', :profile => user.identifier)) %>
8 8 <% end %>
9 9 <% end %>
10 10  
... ...
app/views/spam/index.rhtml 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +<h1><%= _('Manage SPAM') %></h1>
  2 +
  3 +<% button_bar do %>
  4 + <%= button :back, _('Back to control panel'), :controller => :profile_editor %>
  5 +<% end %>
  6 +
  7 +<%# FIXME should not need to replicate the article structure like this to be able to use the same formatting as the comments listing %>
  8 +<div id='article'>
  9 + <div class="comments" id="comments_list">
  10 + <ul class="article-comments-list">
  11 + <%= render :partial => 'content_viewer/comment', :collection => @spam %>
  12 + </ul>
  13 + </div>
  14 +</div>
  15 +
  16 +<%= pagination_links @spam %>
  17 +
  18 +<% button_bar do %>
  19 + <%= button :back, _('Back to control panel'), :controller => :profile_editor %>
  20 +<% end %>
... ...
app/views/tasks/_task.rhtml
... ... @@ -50,13 +50,13 @@
50 50 <% fields_for "tasks[#{task.id}][task]", task do |f| %>
51 51 <% if task.accept_details %>
52 52 <div id="on-accept-information-<%=task.id%>" style="display: none">
53   - <%= render :partial => partial_for_task_class(task.class, :accept_details), :locals => {:task => task, :f => f} %>
  53 + <%= render :partial => partial_for_class(task.class, :accept_details), :locals => {:task => task, :f => f} %>
54 54 </div>
55 55 <% end %>
56 56  
57 57 <% if task.reject_details %>
58 58 <div id="on-reject-information-<%=task.id%>" style="display: none">
59   - <%= render :partial => partial_for_task_class(task.class, :reject_details), :locals => {:task => task, :f => f} %>
  59 + <%= render :partial => partial_for_class(task.class, :reject_details), :locals => {:task => task, :f => f} %>
60 60 </div>
61 61 <% end %>
62 62 <% end %>
... ...
app/views/tasks/processed.rhtml
... ... @@ -7,7 +7,7 @@
7 7 <ul>
8 8 <% @tasks.each do |item| %>
9 9 <li>
10   - <strong><%= item.information %></strong> <br/>
  10 + <strong><%= task_information(item) %></strong> <br/>
11 11 <small>
12 12 <%= _('Created:') +' '+ show_date(item.created_at) %>
13 13 &nbsp; &#151; &nbsp;
... ...
config/initializers/action_tracker.rb
... ... @@ -23,7 +23,28 @@ ActionTrackerConfig.verbs = {
23 23 },
24 24  
25 25 :upload_image => {
26   - :description => lambda { n_('uploaded 1 image<br />%{thumbnails}<br style="clear: both;" />', 'uploaded %{num} images<br />%{thumbnails}<br style="clear: both;" />', get_view_url.size) % { :num => get_view_url.size, :thumbnails => '{{ta.collect_group_with_index(:thumbnail_path){ |t,i| content_tag(:span, link_to(image_tag(t), ta.get_view_url[i]))}.last(3).join}}' } },
  26 + :description => lambda do
  27 + total = get_view_url.size
  28 + n_('uploaded 1 image', 'uploaded %d images', total) % total +
  29 + '<br />{{'+
  30 + 'ta.collect_group_with_index(:thumbnail_path) { |t,i|' +
  31 + " if ( #{total} == 1 );" +
  32 + ' link_to( image_tag(t), ta.get_view_url[i], :class => \'upimg\' );' +
  33 + ' else;' +
  34 + " pos = #{total}-i;" +
  35 + ' morethen2 = pos>2 ? \'morethen2\' : \'\';' +
  36 + ' morethen5 = pos>5 ? \'morethen5\' : \'\';' +
  37 + ' t = t.gsub(/(.*)(display)(.*)/, \'\\1thumb\\3\');' +
  38 + ' link_to( \'&nbsp;\', ta.get_view_url[i],' +
  39 + ' :style => "background-image:url(#{t})",' +
  40 + ' :class => "upimg pos#{pos} #{morethen2} #{morethen5}" );' +
  41 + ' end' +
  42 + '}.reverse.join}}' +
  43 + ( total > 5 ?
  44 + '<span class="more" onclick="this.parentNode.className+=\' show-all\'">' +
  45 + '&hellip;</span>' : '' ) +
  46 + '<br style="clear: both;" />'
  47 + end,
27 48 :type => :groupable
28 49 },
29 50  
... ...
config/initializers/plugins.rb
1 1 require 'noosfero/plugin'
2   -require 'noosfero/plugin/acts_as_having_hotspots'
  2 +require 'noosfero/plugin/hot_spot'
3 3 require 'noosfero/plugin/manager'
4   -require 'noosfero/plugin/context'
5 4 require 'noosfero/plugin/active_record'
6 5 require 'noosfero/plugin/mailer_base'
7 6 Noosfero::Plugin.init_system if $NOOSFERO_LOAD_PLUGINS
... ...
config/routes.rb
... ... @@ -19,6 +19,7 @@ ActionController::Routing::Routes.draw do |map|
19 19  
20 20 # -- just remember to delete public/index.html.
21 21 # You can have the root of your site routed by hooking up ''
  22 + map.root :controller => "home", :conditions => { :if => lambda { |env| !Domain.hosting_profile_at(env[:host]) } }
22 23 map.connect '', :controller => "home", :conditions => { :if => lambda { |env| !Domain.hosting_profile_at(env[:host]) } }
23 24 map.home 'site/:action', :controller => 'home'
24 25  
... ... @@ -121,9 +122,12 @@ ActionController::Routing::Routes.draw do |map|
121 122 # cache stuff - hack
122 123 map.cache 'public/:action/:id', :controller => 'public'
123 124  
  125 + map.connect ':profile/edit_comment/:id/*page', :controller => 'content_viewer', :action => 'edit_comment', :profile => /#{Noosfero.identifier_format}/
  126 +
124 127 # match requests for profiles that don't have a custom domain
125 128 map.homepage ':profile/*page', :controller => 'content_viewer', :action => 'view_page', :profile => /#{Noosfero.identifier_format}/, :conditions => { :if => lambda { |env| !Domain.hosting_profile_at(env[:host]) } }
126 129  
  130 +
127 131 # match requests for content in domains hosted for profiles
128 132 map.connect '*page', :controller => 'content_viewer', :action => 'view_page'
129 133  
... ...
db/migrate/20120825185219_add_user_agent_and_referrer_to_comments.rb 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +class AddUserAgentAndReferrerToComments < ActiveRecord::Migration
  2 + def self.up
  3 + add_column :comments, :user_agent, :string
  4 + add_column :comments, :referrer, :string
  5 + end
  6 +
  7 + def self.down
  8 + remove_column :comments, :user_agent
  9 + remove_column :comments, :referrer
  10 + end
  11 +end
... ...
db/schema.rb
... ... @@ -9,7 +9,7 @@
9 9 #
10 10 # It's strongly recommended to check this file into your version control system.
11 11  
12   -ActiveRecord::Schema.define(:version => 20120823215007) do
  12 +ActiveRecord::Schema.define(:version => 20120825185219) do
13 13  
14 14 create_table "abuse_reports", :force => true do |t|
15 15 t.integer "reporter_id"
... ... @@ -213,6 +213,8 @@ ActiveRecord::Schema.define(:version =&gt; 20120823215007) do
213 213 t.string "ip_address"
214 214 t.boolean "spam"
215 215 t.string "source_type"
  216 + t.string "user_agent"
  217 + t.string "referrer"
216 218 end
217 219  
218 220 create_table "contact_lists", :force => true do |t|
... ...
debian/changelog
  1 +noosfero (0.39.0~1) UNRELEASED; urgency=low
  2 +
  3 + * Pre-release to test the antispam mechanism.
  4 +
  5 + -- Antonio Terceiro <terceiro@debian.org> Thu, 30 Aug 2012 14:55:10 -0300
  6 +
  7 +noosfero (0.38.2) unstable; urgency=low
  8 +
  9 + * Bugfixes release
  10 +
  11 + -- Antonio Terceiro <terceiro@colivre.coop.br> Wed, 05 Sep 2012 10:07:58 -0300
  12 +
1 13 noosfero (0.38.1) unstable; urgency=low
2 14  
3 15 * Bugfixes release
... ...
debian/control
... ... @@ -62,8 +62,7 @@ Description: free web-based platform for social networks
62 62  
63 63 Package: noosfero-apache
64 64 Architecture: all
65   -Depends: apache2, debconf
66   -Suggests: noosfero
  65 +Depends: apache2, debconf, noosfero
67 66 Description: free web-based platform for social networks (apache frontend)
68 67 Noosfero is a web platform for social and solidarity economy networks with
69 68 blog, e-Porfolios, CMS, RSS, thematic discussion, events agenda and collective
... ...
debian/noosfero-console
... ... @@ -2,4 +2,7 @@
2 2  
3 3 set -e
4 4  
5   -cd /usr/share/noosfero && su noosfero -c "./script/console production"
  5 +environment="$1"
  6 +test -z "$environment" && environment=production
  7 +
  8 +cd /usr/share/noosfero && su noosfero -c "./script/console $environment"
... ...
etc/noosfero/varnish-noosfero.vcl
1 1 sub vcl_recv {
  2 + if (req.request == "GET" || req.request == "HEAD") {
2 3 if (req.http.Cookie) {
3   - # We only care about the "_noosfero_session.*" cookie, used for
4   - # authentication.
5   - if (req.http.Cookie ~ "_noosfero_session.*" ) {
6   - return (pass);
7   - }
8   - # Else strip all cookies
  4 + # We only care about the "_noosfero_session.*" cookie, used for
  5 + # authentication.
  6 + if (req.http.Cookie !~ "_noosfero_session.*" ) {
  7 + # strip all cookies
9 8 unset req.http.Cookie;
  9 + }
10 10 }
  11 + }
11 12 }
12 13  
13 14 sub vcl_error {
... ...
features/edit_environment_templates.feature
... ... @@ -8,7 +8,7 @@ Feature: edit environment templates
8 8 Scenario: See links to edit all templates
9 9 Given I am logged in as admin
10 10 When I follow "Administration"
11   - And I follow "Edit Templates"
  11 + And I follow "Profile templates"
12 12 Then I should see "Person template" link
13 13 And I should see "Community template" link
14 14 And I should see "Enterprise template" link
... ... @@ -17,28 +17,28 @@ Feature: edit environment templates
17 17 Scenario: Go to control panel of person template
18 18 Given I am logged in as admin
19 19 When I follow "Administration"
20   - And I follow "Edit Templates"
  20 + And I follow "Profile templates"
21 21 And I follow "Person template"
22 22 Then I should be on Person template's control panel
23 23  
24 24 Scenario: Go to control panel of enterprise template
25 25 Given I am logged in as admin
26 26 When I follow "Administration"
27   - And I follow "Edit Templates"
  27 + And I follow "Profile templates"
28 28 And I follow "Enterprise template"
29 29 Then I should be on Enterprise template's control panel
30 30  
31 31 Scenario: Go to control panel of inactive enterprise template
32 32 Given I am logged in as admin
33 33 When I follow "Administration"
34   - And I follow "Edit Templates"
  34 + And I follow "Profile templates"
35 35 And I follow "Inactive enterprise template"
36 36 Then I should be on Inactive Enterprise template's control panel
37 37  
38 38 Scenario: Go to control panel of community template
39 39 Given I am logged in as admin
40 40 When I follow "Administration"
41   - And I follow "Edit Templates"
  41 + And I follow "Profile templates"
42 42 And I follow "Community template"
43 43 Then I should be on Community template's control panel
44 44  
... ... @@ -46,7 +46,7 @@ Feature: edit environment templates
46 46 Given that the default environment have no Inactive Enterprise template
47 47 And I am logged in as admin
48 48 When I follow "Administration"
49   - And I follow "Edit Templates"
  49 + And I follow "Profile templates"
50 50 Then I should see "Person template" link
51 51 And I should see "Community template" link
52 52 And I should see "Enterprise template" link
... ...
features/environment_name.feature
... ... @@ -6,7 +6,7 @@ Feature: setting environment name
6 6 Scenario: setting environment name through administration panel
7 7 Given I am logged in as admin
8 8 When I follow "Administration"
9   - And I follow "Edit environment settings"
  9 + And I follow "Environment settings"
10 10 And I fill in "Site name" with "My environment"
11 11 And I press "Save"
12 12 Then I should see "My environment" within "title"
... ...
features/export_users.feature
... ... @@ -10,14 +10,14 @@ Feature: export users
10 10 Scenario: Export users as XML
11 11 Given I am logged in as admin
12 12 When I follow "Administration"
13   - And I follow "Manage users"
  13 + And I follow "Users"
14 14 And I follow "[XML]"
15 15 Then I should see "ultraje"
16 16  
17 17 Scenario: Export users as CSV
18 18 Given I am logged in as admin
19 19 When I follow "Administration"
20   - And I follow "Manage users"
  20 + And I follow "Users"
21 21 And I follow "[CSV]"
22 22 Then I should see "name;email"
23 23 And I should see "ultraje"
... ...
features/manage_categories.feature
... ... @@ -14,7 +14,7 @@ Feature: manage categories
14 14 | Development | services |
15 15 And I am logged in as admin
16 16 And I am on the environment control panel
17   - And I follow "Manage categories"
  17 + And I follow "Categories"
18 18  
19 19 Scenario: load only top level categories
20 20 Then I should see "Products"
... ...
features/plugins.feature
... ... @@ -49,7 +49,7 @@ Feature: plugins
49 49 When I go to the profile
50 50 Then I should see "Test plugin tab"
51 51 And I go to the environment control panel
52   - And I follow "Enable/disable plugins"
  52 + And I follow "Plugins"
53 53 And I uncheck "Test plugin"
54 54 And I press "Save changes"
55 55 When I go to the Control panel
... ...
features/roles.feature
... ... @@ -5,26 +5,26 @@ Feature: manage roles
5 5 Scenario: create new role
6 6 Given I am logged in as admin
7 7 And I go to the environment control panel
8   - And I follow "Manage User roles"
  8 + And I follow "User roles"
9 9 Then I should not see "My new role"
10 10 And I follow "Create a new role"
11 11 And I fill in "Name" with "My new role"
12 12 And I check "Publish content"
13 13 And I press "Create role"
14 14 And I go to the environment control panel
15   - And I follow "Manage User roles"
  15 + And I follow "User roles"
16 16 Then I should see "My new role"
17 17  
18 18 Scenario: edit a role
19 19 Given I am logged in as admin
20 20 And I go to the environment control panel
21   - And I follow "Manage User roles"
  21 + And I follow "User roles"
22 22 Then I should not see "My new role"
23 23 And I follow "Profile Administrator"
24 24 And I follow "Edit"
25 25 And I fill in "Name" with "My new role"
26 26 And I press "Save changes"
27 27 And I go to the environment control panel
28   - And I follow "Manage User roles"
  28 + And I follow "User roles"
29 29 Then I should see "My new role"
30 30 And I should not see "Profile Administrator"
... ...
features/send_email_to_environment_members.feature
... ... @@ -18,7 +18,7 @@ Feature: send emails to environment members users
18 18 Scenario: Send e-mail to members
19 19 Given I am logged in as admin
20 20 When I follow "Administration"
21   - And I follow "Manage users"
  21 + And I follow "Users"
22 22 And I follow "Send e-mail to users"
23 23 And I fill in "Subject" with "Hello, user!"
24 24 And I fill in "body" with "We have some news"
... ... @@ -28,7 +28,7 @@ Feature: send emails to environment members users
28 28 Scenario: Not send e-mail to members if subject is blank
29 29 Given I am logged in as admin
30 30 When I follow "Administration"
31   - And I follow "Manage users"
  31 + And I follow "Users"
32 32 And I follow "Send e-mail to users"
33 33 And I fill in "body" with "We have some news"
34 34 When I press "Send"
... ... @@ -37,7 +37,7 @@ Feature: send emails to environment members users
37 37 Scenario: Not send e-mail to members if body is blank
38 38 Given I am logged in as admin
39 39 When I follow "Administration"
40   - And I follow "Manage users"
  40 + And I follow "Users"
41 41 And I follow "Send e-mail to users"
42 42 And I fill in "Subject" with "Hello, user!"
43 43 When I press "Send"
... ... @@ -46,7 +46,7 @@ Feature: send emails to environment members users
46 46 Scenario: Cancel creation of mailing
47 47 Given I am logged in as admin
48 48 When I follow "Administration"
49   - And I follow "Manage users"
  49 + And I follow "Users"
50 50 And I follow "Send e-mail to users"
51 51 Then I should be on /admin/users/send_mail
52 52 When I follow "Cancel e-mail"
... ...
lib/acts_as_faceted.rb
... ... @@ -8,7 +8,7 @@ module ActsAsFaceted
8 8 #
9 9 #acts_as_faceted :fields => {
10 10 # :f_type => {:label => _('Type'), :proc => proc{|klass| f_type_proc(klass)}},
11   - # :f_published_at => {:type => :date, :label => _('Published date'), :queries => {'[* TO NOW-1YEARS/DAY]' => _("Older than one year"),
  11 + # :f_published_at => {:type => :date, :label => _('Published date'), :queries => {'[* TO NOW-1YEARS/DAY]' => _("Older than one year"),
12 12 # '[NOW-1YEARS TO NOW/DAY]' => _("Last year"), '[NOW-1MONTHS TO NOW/DAY]' => _("Last month"), '[NOW-7DAYS TO NOW/DAY]' => _("Last week"), '[NOW-1DAYS TO NOW/DAY]' => _("Last day")}},
13 13 # :f_profile_type => {:label => _('Author'), :proc => proc{|klass| f_profile_type_proc(klass)}},
14 14 # :f_category => {:label => _('Categories')}},
... ... @@ -36,7 +36,7 @@ module ActsAsFaceted
36 36 self.facets_order = options[:order] || self.facets.keys
37 37 self.facets_results_containers = {:fields => 'facet_fields', :queries => 'facet_queries', :ranges => 'facet_ranges'}
38 38 self.facets_option_for_solr = Hash[facets.select{ |id,data| ! data.has_key?(:queries) }].keys
39   - self.facets_fields_for_solr = facets.map{ |id,data| {id => data[:type] || :facet} }
  39 + self.facets_fields_for_solr = facets.map{ |id,data| {id => data[:type] || :facet} }
40 40 self.solr_fields_names = facets.map{ |id,data| id.to_s + '_' + get_solr_field_type(data[:type] || :facet) }
41 41 self.facet_category_query = options[:category_query]
42 42  
... ... @@ -67,6 +67,7 @@ module ActsAsFaceted
67 67 raise 'Use map_facets_for before this method' if facet[:solr_field].nil?
68 68 facets_data = {} if facets_data.blank? # could be empty array
69 69 solr_facet = to_solr_fields_names[facet[:solr_field]]
  70 + unfiltered_facets_data ||= {}
70 71  
71 72 if facet[:queries]
72 73 container = facets_data[facets_results_containers[:queries]]
... ... @@ -158,13 +159,15 @@ module ActsAsFaceted
158 159 end
159 160  
160 161 def facet_label(facet)
161   - _ facet[:label]
  162 + return nil unless facet
  163 + _(facet[:label])
162 164 end
163 165  
164 166 def facets_find_options(facets_selected = {}, options = {})
165 167 browses = []
166 168 facets_selected ||= {}
167 169 facets_selected.map do |id, value|
  170 + next unless facets[id.to_sym]
168 171 if value.kind_of?(Hash)
169 172 value.map do |label_id, value|
170 173 value.to_a.each do |value|
... ...
lib/needs_profile.rb
... ... @@ -14,12 +14,12 @@ module NeedsProfile
14 14 profile || environment # prefers profile, but defaults to environment
15 15 end
16 16  
17   - protected
18   -
19 17 def profile
20 18 @profile
21 19 end
22 20  
  21 + protected
  22 +
23 23 def load_profile
24 24 @profile ||= environment.profiles.find_by_identifier(params[:profile])
25 25 if @profile
... ...
lib/noosfero.rb
... ... @@ -2,7 +2,7 @@ require &#39;fast_gettext&#39;
2 2  
3 3 module Noosfero
4 4 PROJECT = 'noosfero'
5   - VERSION = '0.38.1'
  5 + VERSION = '0.39.0~1'
6 6  
7 7 def self.pattern_for_controllers_in_directory(dir)
8 8 disjunction = controllers_in_directory(dir).join('|')
... ...
lib/noosfero/plugin.rb
... ... @@ -12,17 +12,41 @@ class Noosfero::Plugin
12 12 end
13 13  
14 14 def init_system
15   - Dir.glob(File.join(Rails.root, 'config', 'plugins', '*')).select do |entry|
  15 + enabled_plugins = Dir.glob(File.join(Rails.root, 'config', 'plugins', '*'))
  16 + if Rails.env.test? && !enabled_plugins.include?(File.join(Rails.root, 'config', 'plugins', 'foo'))
  17 + enabled_plugins << File.join(Rails.root, 'plugins', 'foo')
  18 + end
  19 + enabled_plugins.select do |entry|
16 20 File.directory?(entry)
17 21 end.each do |dir|
18   - Rails.configuration.controller_paths << File.join(dir, 'controllers')
19   - ActiveSupport::Dependencies.load_paths << File.join(dir, 'controllers')
20   - [ ActiveSupport::Dependencies.load_paths, $:].each do |path|
21   - path << File.join(dir, 'models')
22   - path << File.join(dir, 'lib')
  22 + plugin_name = File.basename(dir)
  23 +
  24 + plugin_dependencies_ok = true
  25 + plugin_dependencies_file = File.join(dir, 'dependencies.rb')
  26 + if File.exists?(plugin_dependencies_file)
  27 + begin
  28 + require plugin_dependencies_file
  29 + rescue LoadError => ex
  30 + plugin_dependencies_ok = false
  31 + $stderr.puts "W: Noosfero plugin #{plugin_name} failed to load (#{ex})"
  32 + end
23 33 end
24 34  
25   - klass(File.basename(dir))
  35 + if plugin_dependencies_ok
  36 + Rails.configuration.controller_paths << File.join(dir, 'controllers')
  37 + ActiveSupport::Dependencies.load_paths << File.join(dir, 'controllers')
  38 + controllers_folders = %w[public profile myprofile admin]
  39 + controllers_folders.each do |folder|
  40 + Rails.configuration.controller_paths << File.join(dir, 'controllers', folder)
  41 + ActiveSupport::Dependencies.load_paths << File.join(dir, 'controllers', folder)
  42 + end
  43 + [ ActiveSupport::Dependencies.load_paths, $:].each do |path|
  44 + path << File.join(dir, 'models')
  45 + path << File.join(dir, 'lib')
  46 + end
  47 +
  48 + klass(plugin_name)
  49 + end
26 50 end
27 51 end
28 52  
... ... @@ -196,28 +220,6 @@ class Noosfero::Plugin
196 220 nil
197 221 end
198 222  
199   - # This is a generic hotspot for all controllers on Noosfero.
200   - # If any plugin wants to define filters to run on any controller, the name of
201   - # the hotspot must be in the following form: <underscored_controller_name>_filters.
202   - # Example: for ProfileController the hotspot is profile_controller_filters
203   - #
204   - # -> Adds a filter to a controller
205   - # returns = { :type => type,
206   - # :method_name => method_name,
207   - # :options => {:opt1 => opt1, :opt2 => opt2},
208   - # :block => Proc or lambda block}
209   - # type = 'before_filter' or 'after_filter'
210   - # method_name = The name of the filter
211   - # option = Filter options, like :only or :except
212   - # block = Block that the filter will call
213   - def method_missing(method, *args, &block)
214   - if method.to_s =~ /^(.+)_controller_filters$/
215   - []
216   - else
217   - super
218   - end
219   - end
220   -
221 223 # This method will be called just before a comment is saved to the database.
222 224 #
223 225 # It can modify the comment in several ways. In special, a plugin can call
... ... @@ -226,16 +228,53 @@ class Noosfero::Plugin
226 228 # example:
227 229 #
228 230 # def filter_comment(comment)
229   - # comment.reject! if anti_spam_service.is_spam?(comment)
  231 + # if user_not_logged_in
  232 + # comment.reject!
  233 + # end
230 234 # end
231 235 #
232 236 def filter_comment(comment)
233 237 end
234 238  
235   - # This method will be called just after a comment has been saved to the
236   - # database, so that a plugin can perform some action on it.
  239 + # This method is called by the CommentHandler background job before sending
  240 + # the notification email. If the comment is marked as spam (i.e. by calling
  241 + # <tt>comment.spam!</tt>), then the notification email will *not* be sent.
  242 + #
  243 + # example:
  244 + #
  245 + # def check_comment_for_spam(comment)
  246 + # if anti_spam_service.is_spam?(comment)
  247 + # comment.spam!
  248 + # end
  249 + # end
  250 + #
  251 + def check_comment_for_spam(comment)
  252 + end
  253 +
  254 + # This method is called when the user manually marks a comment as SPAM. A
  255 + # plugin implementing this method should train its spam detection mechanism
  256 + # by submitting this comment as a confirmed spam.
  257 + #
  258 + # example:
  259 + #
  260 + # def comment_marked_as_spam(comment)
  261 + # anti_spam_service.train_with_spam(comment)
  262 + # end
  263 + #
  264 + def comment_marked_as_spam(comment)
  265 + end
  266 +
  267 + # This method is called when the user manually marks a comment a NOT SPAM. A
  268 + # plugin implementing this method should train its spam detection mechanism
  269 + # by submitting this coimment as a confirmed ham.
  270 + #
  271 + # example:
  272 + #
  273 + # def comment_marked_as_ham(comment)
  274 + # anti_spam_service.train_with_ham(comment)
  275 + # end
237 276 #
238   - def comment_saved(comment)
  277 + def comment_marked_as_ham(comment)
239 278 end
240 279  
241 280 # -> Adds fields to the signup form
... ... @@ -281,4 +320,71 @@ class Noosfero::Plugin
281 320 nil
282 321 end
283 322  
  323 + # -> Add an alternative authentication method.
  324 + # Your plugin have to make the access control and return the logged user.
  325 + # returns = User
  326 + def alternative_authentication
  327 + nil
  328 + end
  329 +
  330 + # -> Adds adicional link to make the user authentication
  331 + # returns = lambda block that creates html code
  332 + def alternative_authentication_link
  333 + nil
  334 + end
  335 +
  336 + # -> Allow or not user registration
  337 + # returns = boolean
  338 + def allow_user_registration
  339 + true
  340 + end
  341 +
  342 + # -> Allow or not password recovery by users
  343 + # returns = boolean
  344 + def allow_password_recovery
  345 + true
  346 + end
  347 +
  348 + # -> Adds fields to the login form
  349 + # returns = lambda block that creates html code
  350 + def login_extra_contents
  351 + nil
  352 + end
  353 +
  354 + def method_missing(method, *args, &block)
  355 + # This is a generic hotspot for all controllers on Noosfero.
  356 + # If any plugin wants to define filters to run on any controller, the name of
  357 + # the hotspot must be in the following form: <underscored_controller_name>_filters.
  358 + # Example: for ProfileController the hotspot is profile_controller_filters
  359 + #
  360 + # -> Adds a filter to a controller
  361 + # returns = { :type => type,
  362 + # :method_name => method_name,
  363 + # :options => {:opt1 => opt1, :opt2 => opt2},
  364 + # :block => Proc or lambda block}
  365 + # type = 'before_filter' or 'after_filter'
  366 + # method_name = The name of the filter
  367 + # option = Filter options, like :only or :except
  368 + # block = Block that the filter will call
  369 + if method.to_s =~ /^(.+)_controller_filters$/
  370 + []
  371 + # -> Removes the action button from the content
  372 + # returns = boolean
  373 + elsif method.to_s =~ /^content_remove_(#{content_actions.join('|')})$/
  374 + nil
  375 + # -> Expire the action button from the content
  376 + # returns = string with reason of expiration
  377 + elsif method.to_s =~ /^content_expire_(#{content_actions.join('|')})$/
  378 + nil
  379 + else
  380 + super
  381 + end
  382 + end
  383 +
  384 + private
  385 +
  386 + def content_actions
  387 + %w[edit delete spread locale suggest home]
  388 + end
  389 +
284 390 end
... ...
lib/noosfero/plugin/acts_as_having_hotspots.rb
... ... @@ -1,44 +0,0 @@
1   -module ActsAsHavingHotspots
2   - module ClassMethods
3   - # Adding this feature to a class demands that it defines an instance method
4   - # 'environment' that returns the environment associated with the instance.
5   - def acts_as_having_hotspots
6   - send :include, InstanceMethods
7   - end
8   -
9   - module InstanceMethods
10   - # Dispatches +event+ to each enabled plugin and collect the results.
11   - #
12   - # Returns an Array containing the objects returned by the event method in
13   - # each plugin. This array is compacted (i.e. nils are removed) and flattened
14   - # (i.e. elements of arrays are added to the resulting array). For example, if
15   - # the enabled plugins return 1, 0, nil, and [1,2,3], then this method will
16   - # return [1,0,1,2,3]
17   - #
18   - def dispatch(event, *args)
19   - enabled_plugins.map { |plugin| plugin.send(event, *args) }.compact.flatten
20   - end
21   -
22   - # Dispatch without flatten since scopes are executed if you run flatten on them
23   - def dispatch_scopes(event, *args)
24   - enabled_plugins.map { |plugin| plugin.send(event, *args) }.compact
25   - end
26   -
27   - def enabled_plugins
28   - Thread.current[:enabled_plugins] ||= (Noosfero::Plugin.all & environment.enabled_plugins).map do |plugin_name|
29   - plugin = plugin_name.constantize.new
30   - plugin.context = context
31   - plugin
32   - end
33   - end
34   -
35   - if !method_defined?(:context)
36   - define_method(:context) do
37   - Noosfero::Plugin::Context.new
38   - end
39   - end
40   - end
41   - end
42   -end
43   -
44   -ActiveRecord::Base.send(:extend, ActsAsHavingHotspots::ClassMethods)
lib/noosfero/plugin/context.rb
... ... @@ -1,15 +0,0 @@
1   -# This class defines the interface to important context information from the
2   -# controller that can be accessed by plugins
3   -class Noosfero::Plugin::Context
4   -
5   - def initialize(controller = ApplicationController.new)
6   - @controller = controller
7   - end
8   -
9   - delegate :profile, :request, :response, :environment, :params, :session, :user, :logged_in?, :to => :controller
10   -
11   - protected
12   -
13   - attr_reader :controller
14   -
15   -end
lib/noosfero/plugin/hot_spot.rb 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +# This module must be included by classes that contain Noosfero plugin
  2 +# hotspots.
  3 +#
  4 +# Classes that include this module *must* provide a method called
  5 +# <tt>environment</tt> which returns an intance of Environment. This
  6 +# Environment will be used to determine which plugins are enabled and therefore
  7 +# which plugins should be instantiated.
  8 +module Noosfero::Plugin::HotSpot
  9 +
  10 + # Returns an instance of Noosfero::Plugin::Manager.
  11 + #
  12 + # This which is intantiated on the first call and just returned in subsequent
  13 + # calls.
  14 + def plugins
  15 + @plugins ||= Noosfero::Plugin::Manager.new(environment, self)
  16 + end
  17 +
  18 +end
... ...
lib/noosfero/plugin/manager.rb
1 1 class Noosfero::Plugin::Manager
2 2  
3   - extend ActsAsHavingHotspots::ClassMethods
4   - acts_as_having_hotspots
5   -
  3 + attr_reader :environment
6 4 attr_reader :context
7 5  
8   - delegate :environment, :to => :context
  6 + def initialize(environment, context)
  7 + @environment = environment
  8 + @context = context
  9 + end
  10 +
9 11 delegate :each, :to => :enabled_plugins
10 12 include Enumerable
11 13  
12   - def initialize(controller)
13   - @context = Noosfero::Plugin::Context.new(controller)
14   - Thread.current[:enabled_plugins] = (Noosfero::Plugin.all & environment.enabled_plugins).map do |plugin_name|
15   - plugin = plugin_name.constantize.new
16   - plugin.context = context
17   - plugin
  14 + # Dispatches +event+ to each enabled plugin and collect the results.
  15 + #
  16 + # Returns an Array containing the objects returned by the event method in
  17 + # each plugin. This array is compacted (i.e. nils are removed) and flattened
  18 + # (i.e. elements of arrays are added to the resulting array). For example, if
  19 + # the enabled plugins return 1, 0, nil, and [1,2,3], then this method will
  20 + # return [1,0,1,2,3]
  21 + #
  22 + def dispatch(event, *args)
  23 + dispatch_without_flatten(event, *args).flatten
  24 + end
  25 +
  26 + def dispatch_without_flatten(event, *args)
  27 + map { |plugin| plugin.send(event, *args) }.compact
  28 + end
  29 +
  30 + alias :dispatch_scopes :dispatch_without_flatten
  31 +
  32 + def enabled_plugins
  33 + @enabled_plugins ||= (Noosfero::Plugin.all & environment.enabled_plugins).map do |plugin|
  34 + p = plugin.constantize.new
  35 + p.context = context
  36 + p
18 37 end
19 38 end
20 39  
... ...
lib/noosfero/plugin/routes.rb
1   -Dir.glob(File.join(Rails.root, 'config', 'plugins', '*', 'controllers')) do |dir|
2   - plugin_name = File.basename(File.dirname(dir))
  1 +plugins_root = Rails.env.test? ? 'plugins' : File.join('config', 'plugins')
  2 +
  3 +Dir.glob(File.join(Rails.root, plugins_root, '*', 'controllers')) do |controllers_dir|
  4 + prefixes_by_folder = {'public' => 'plugin',
  5 + 'profile' => 'profile/:profile/plugin',
  6 + 'myprofile' => 'myprofile/:profile/plugin',
  7 + 'admin' => 'admin/plugin'}
  8 +
  9 + controllers_by_folder = prefixes_by_folder.keys.inject({}) do |hash, folder|
  10 + hash.merge!({folder => Dir.glob(File.join(controllers_dir, folder, '*')).map {|full_names| File.basename(full_names).gsub(/_controller.rb$/,'')}})
  11 + end
  12 +
  13 + plugin_name = File.basename(File.dirname(controllers_dir))
  14 +
  15 + controllers_by_folder.each do |folder, controllers|
  16 + controllers.each do |controller|
  17 + controller_name = controller.gsub("#{plugin_name}_plugin_",'')
  18 + map.connect "#{prefixes_by_folder[folder]}/#{plugin_name}/#{controller_name}/:action/:id", :controller => controller
  19 + end
  20 + end
  21 +
3 22 map.connect 'plugin/' + plugin_name + '/:action/:id', :controller => plugin_name + '_plugin'
4   - map.connect 'profile/:profile/plugins/' + plugin_name + '/:action/:id', :controller => plugin_name + '_plugin_profile'
  23 + map.connect 'profile/:profile/plugin/' + plugin_name + '/:action/:id', :controller => plugin_name + '_plugin_profile'
5 24 map.connect 'myprofile/:profile/plugin/' + plugin_name + '/:action/:id', :controller => plugin_name + '_plugin_myprofile'
6 25 map.connect 'admin/plugin/' + plugin_name + '/:action/:id', :controller => plugin_name + '_plugin_admin'
7 26 end
8   -
... ...
lib/tasks/data.rake
... ... @@ -3,7 +3,7 @@ namespace :db do
3 3 task :minimal do
4 4 sh './script/runner', "Environment.create!(:name => 'Noosfero', :is_default => true)"
5 5 unless ENV['NOOSFERO_DOMAIN'].blank?
6   - sh './script/runner', "environment.domains << Domain.new(:name => ENV['NOOSFERO_DOMAIN'])"
  6 + sh './script/runner', "Environment.default.domains << Domain.new(:name => ENV['NOOSFERO_DOMAIN'])"
7 7 end
8 8 end
9 9 end
... ...
lib/tasks/plugins_tests.rake
1   -@disabled_plugins = Dir.glob(File.join(Rails.root, 'plugins', '*')).map { |file| File.basename(file)} - Dir.glob(File.join(Rails.root, 'config', 'plugins', '*')).map { |file| File.basename(file)}
2   -@disabled_plugins.delete('template')
3   -
4   -def define_task(test, plugins_folder='plugins', plugin = '*')
5   - test_files = Dir.glob(File.join(Rails.root, plugins_folder, plugin, 'test', test[:folder], '**', '*_test.rb'))
6   - desc 'Runs ' + (plugin != '*' ? plugin : 'plugins') + ' ' + test[:name] + ' tests'
7   - Rake::TestTask.new(test[:name].to_sym => 'db:test:plugins:prepare') do |t|
8   - t.libs << 'test'
9   - t.test_files = test_files
10   - t.verbose = true
11   - end
  1 +all_plugins = Dir.glob('plugins/*').map { |f| File.basename(f) } - ['template']
  2 +def enabled_plugins
  3 + Dir.glob('config/plugins/*').map { |f| File.basename(f) } - ['README']
12 4 end
  5 +disabled_plugins = all_plugins - enabled_plugins
13 6  
14 7 task 'db:test:plugins:prepare' do
15   - Rake::Task['db:test:prepare'].invoke
16   - sh 'rake db:migrate RAILS_ENV=test SCHEMA=/dev/null'
  8 + if Dir.glob('config/plugins/*/db/migrate/*.rb').empty?
  9 + puts "I: skipping database setup, enabled plugins have no migrations"
  10 + else
  11 + Rake::Task['db:test:prepare'].invoke
  12 + sh 'rake db:migrate RAILS_ENV=test SCHEMA=/dev/null'
  13 + end
17 14 end
18 15  
19   -namespace :test do
20   - namespace :noosfero_plugins do
21   - tasks = [
22   - {:name => :available, :folder => 'plugins'},
23   - {:name => :enabled, :folder => File.join('config', 'plugins')}
24   - ]
25   - tests = [
26   - {:name => 'units', :folder => 'unit'},
27   - {:name => 'functionals', :folder => 'functional'},
28   - {:name => 'integration', :folder => 'integration'}
29   - ]
30   -
31   - tasks.each do |t|
32   - namespace t[:name] do
33   - tests.each do |test|
34   - define_task(test, t[:folder])
35   - end
36   - end
37   - end
  16 +def plugin_name(plugin)
  17 + "#{plugin} plugin"
  18 +end
  19 +
  20 +def run_tests(name, files_glob)
  21 + files = Dir.glob(files_glob)
  22 + if files.empty?
  23 + puts "I: no tests to run (#{name})"
  24 + else
  25 + sh 'testrb', '-Itest', *files
  26 + end
  27 +end
38 28  
39   - plugins = Dir.glob(File.join(Rails.root, 'plugins', '*')).map {|path| File.basename(path)}
  29 +def run_cucumber(name, profile, files_glob)
  30 + files = Dir.glob(files_glob)
  31 + if files.empty?
  32 + puts "I: no tests to run #{name}"
  33 + else
  34 + sh 'xvfb-run', 'ruby', '-S', 'cucumber', '--profile', profile, '--format', ENV['CUCUMBER_FORMAT'] || 'progress' , *features
  35 + end
  36 +end
40 37  
41   - plugins.each do |plugin_name|
42   - namespace plugin_name do
43   - tests.each do |test|
44   - define_task(test, 'plugins', plugin_name)
45   - end
46   - end
  38 +def plugin_test_task(name, plugin, files_glob)
  39 + desc "Run #{name} tests for #{plugin_name(plugin)}"
  40 + task name => 'db:test:plugins:prepare' do |t|
  41 + run_tests t.name, files_glob
  42 + end
  43 +end
  44 +
  45 +def plugin_cucumber_task(plugin, files_glob)
  46 + task :cucumber => 'db:test:plugins:prepare' do |t|
  47 + run_cucumber t.name, :default, files_glob
  48 + end
  49 +end
47 50  
48   - dependencies = []
49   - tests.each do |test|
50   - dependencies << plugin_name+':'+test[:name]
  51 +def plugin_selenium_task(plugin, files_glob)
  52 + task :selenium => 'db:test:plugins:prepare' do |t|
  53 + run_cucumber t.name, :selenium, files_glob
  54 + end
  55 +end
  56 +
  57 +def test_sequence_task(name, plugin, *tasks)
  58 + desc "Run all tests for #{plugin_name(plugin)}"
  59 + task name do
  60 + failed = []
  61 + tasks.each do |task|
  62 + begin
  63 + Rake::Task['test:noosfero_plugins:' + task.to_s].invoke
  64 + rescue Exception => ex
  65 + puts ex
  66 + failed << task
51 67 end
52   - task plugin_name => dependencies
53 68 end
54   -
55   - task :temp_enable_plugins do
56   - system('./script/noosfero-plugins enableall')
  69 + unless failed.empty?
  70 + fail 'Tests failed: ' + failed.join(', ')
57 71 end
  72 + end
  73 +end
58 74  
59   - task :rollback_temp_enable_plugins do
60   - @disabled_plugins.each { |plugin| system('./script/noosfero-plugins disable ' + plugin)}
  75 +namespace :test do
  76 + namespace :noosfero_plugins do
  77 + all_plugins.each do |plugin|
  78 + namespace plugin do
  79 + plugin_test_task :units, plugin, "plugins/#{plugin}/test/unit/**/*.rb"
  80 + plugin_test_task :functionals, plugin, "plugins/#{plugin}/test/functional/**/*.rb"
  81 + plugin_test_task :integration, plugin, "plugins/#{plugin}/test/integration/**/*.rb"
  82 + plugin_cucumber_task plugin, "plugins/#{plugin}/features/**/*.feature"
  83 + plugin_selenium_task plugin, "plugins/#{plugin}/features/**/*.feature"
  84 + end
  85 +
  86 + test_sequence_task(plugin, plugin, "#{plugin}:units", "#{plugin}:functionals", "#{plugin}:integration", "#{plugin}:cucumber", "#{plugin}:selenium") # FIXME missing cucumber and selenium
61 87 end
62 88  
63   - task :units => 'available:units'
64   - task :functionals => 'available:functionals'
65   - task :integration => 'available:integration'
66   - task :available do
67   - Rake::Task['test:noosfero_plugins:temp_enable_plugins'].invoke
68   - begin
69   - Rake::Task['test:noosfero_plugins:units'].invoke
70   - Rake::Task['test:noosfero_plugins:functionals'].invoke
71   - Rake::Task['test:noosfero_plugins:integration'].invoke
72   - rescue
  89 + { :units => :unit , :functionals => :functional , :integration => :integration }.each do |taskname,folder|
  90 + task taskname => 'db:test:plugins:prepare' do |t|
  91 + run_tests t.name, "plugins/{#{enabled_plugins.join(',')}}/test/#{folder}/**/*.rb"
73 92 end
74   - Rake::Task['test:noosfero_plugins:rollback_temp_enable_plugins'].invoke
75 93 end
76   - task :enabled => ['enabled:units', 'enabled:functionals', 'enabled:integration']
77 94  
  95 + task :cucumber => 'db:test:plugins:prepare' do |t|
  96 + run_cucumber t.name, :default, "plugins/{#{enabled_plugins.join(',')}}/features/**/*.features"
  97 + end
78 98  
79   - namespace :cucumber do
80   - task :enabled do
81   - features = Dir.glob('config/plugins/*/features/*.feature')
82   - if features.empty?
83   - puts "No acceptance tests for enabled plugins, skipping"
84   - else
85   - ruby '-S', 'cucumber', '--format', ENV['CUCUMBER_FORMAT'] || 'progress' , *features
86   - end
87   - end
  99 + task :selenium => 'db:test:plugins:prepare' do |t|
  100 + run_cucumber t.name, :selenium, "plugins/{#{enabled_plugins.join(',')}}/features/**/*.features"
88 101 end
89 102  
90   - namespace :selenium do
91   - task :enabled do
92   - features = Dir.glob('config/plugins/*/features/*.feature')
93   - if features.empty?
94   - puts "No acceptance tests for enabled plugins, skipping"
95   - else
96   - sh 'xvfb-run', 'ruby', '-S', 'cucumber', '--profile', 'selenium', '--format', ENV['CUCUMBER_FORMAT'] || 'progress' , *features
97   - end
98   - end
  103 + task :temp_enable_all_plugins do
  104 + sh './script/noosfero-plugins', 'enableall'
99 105 end
100 106  
  107 + task :rollback_enable_all_plugins do
  108 + sh './script/noosfero-plugins', 'disable', *disabled_plugins
  109 + end
101 110 end
102 111  
103   - task :noosfero_plugins => %w[ noosfero_plugins:available noosfero_plugins:cucumber:enabled noosfero_plugins:selenium:enabled ]
  112 + test_sequence_task(:noosfero_plugins, '*', :temp_enable_all_plugins, :units, :functionals, :integration, :cucumber, :selenium, :rollback_enable_all_plugins)
104 113  
105 114 end
106   -
... ...
lib/tasks/release.rake
... ... @@ -95,6 +95,9 @@ EOF
95 95 sh "cd #{target} && dpkg-buildpackage -us -uc -b"
96 96 end
97 97  
  98 + desc "Build Debian packages (shorcut)"
  99 + task :deb => :debian_packages
  100 +
98 101 desc 'Test Debian package'
99 102 task 'debian:test' => :debian_packages do
100 103 Dir.chdir 'pkg' do
... ...
plugins/anti_spam/controllers/anti_spam_plugin_admin_controller.rb 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +class AntiSpamPluginAdminController < AdminController
  2 + append_view_path File.join(File.dirname(__FILE__) + '/../views')
  3 +
  4 + def index
  5 + @settings = AntiSpamPlugin::Settings.new(environment, params[:settings])
  6 + if request.post?
  7 + @settings.save!
  8 + redirect_to :action => 'index'
  9 + end
  10 + end
  11 +
  12 +end
... ...
plugins/anti_spam/dependencies.rb 0 → 100644
... ... @@ -0,0 +1 @@
  1 +require 'rakismet'
... ...
plugins/anti_spam/lib/anti_spam_plugin.rb 0 → 100644
... ... @@ -0,0 +1,39 @@
  1 +class AntiSpamPlugin < Noosfero::Plugin
  2 +
  3 + def self.plugin_name
  4 + "AntiSpam"
  5 + end
  6 +
  7 + def self.plugin_description
  8 + _("Checks comments against a spam checking service compatible with the Akismet API")
  9 + end
  10 +
  11 + def check_comment_for_spam(comment)
  12 + if rakismet_call(comment, :spam?)
  13 + comment.spam = true
  14 + comment.save!
  15 + end
  16 + end
  17 +
  18 + def comment_marked_as_spam(comment)
  19 + rakismet_call(comment, :spam!)
  20 + end
  21 +
  22 + def comment_marked_as_ham(comment)
  23 + rakismet_call(comment, :ham!)
  24 + end
  25 +
  26 + protected
  27 +
  28 + def rakismet_call(comment, op)
  29 + settings = AntiSpamPlugin::Settings.new(comment.environment)
  30 +
  31 + Rakismet.host = settings.host
  32 + Rakismet.key = settings.api_key
  33 + Rakismet.url = comment.environment.top_url
  34 +
  35 + submission = AntiSpamPlugin::CommentWrapper.new(comment)
  36 + submission.send(op)
  37 + end
  38 +
  39 +end
... ...
plugins/anti_spam/lib/anti_spam_plugin/comment_wrapper.rb 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +class AntiSpamPlugin::CommentWrapper < Struct.new(:comment)
  2 +
  3 + delegate :author_name, :author_email, :title, :body, :ip_address, :user_agent, :referrer, :to => :comment
  4 +
  5 + include Rakismet::Model
  6 +
  7 + alias :author :author_name
  8 + alias :user_ip :ip_address
  9 + alias :content :body
  10 +
  11 +end
... ...
plugins/anti_spam/lib/anti_spam_plugin/settings.rb 0 → 100644
... ... @@ -0,0 +1,35 @@
  1 +class AntiSpamPlugin::Settings
  2 +
  3 + def initialize(environment, attributes = nil)
  4 + @environment = environment
  5 + attributes ||= {}
  6 + attributes.each do |k,v|
  7 + self.send("#{k}=", v)
  8 + end
  9 + end
  10 +
  11 + def settings
  12 + @environment.settings[:anti_spam_plugin] ||= {}
  13 + end
  14 +
  15 + def host
  16 + settings[:host] ||= 'api.antispam.typepad.com'
  17 + end
  18 +
  19 + def host=(value)
  20 + settings[:host] = value
  21 + end
  22 +
  23 + def api_key
  24 + settings[:api_key]
  25 + end
  26 +
  27 + def api_key=(value)
  28 + settings[:api_key] = value
  29 + end
  30 +
  31 + def save!
  32 + @environment.save!
  33 + end
  34 +
  35 +end
... ...
plugins/anti_spam/lib/anti_spam_plugin/spaminator.rb 0 → 100644
... ... @@ -0,0 +1,115 @@
  1 +require 'benchmark'
  2 +
  3 +class AntiSpamPlugin::Spaminator
  4 +
  5 + class << self
  6 + def run(environment)
  7 + instance = new(environment)
  8 + instance.run
  9 + end
  10 +
  11 + def benchmark(environment)
  12 + puts Benchmark.measure { run(environment) }
  13 + end
  14 + end
  15 +
  16 +
  17 + def initialize(environment)
  18 + @environment = environment
  19 + end
  20 +
  21 + def run
  22 + start_time = Time.now
  23 +
  24 + process_all_comments
  25 + process_all_people
  26 + process_people_without_network
  27 +
  28 + finish(start_time)
  29 + end
  30 +
  31 + protected
  32 +
  33 + def finish(start_time)
  34 + @environment.settings[:spaminator_last_run] = start_time
  35 + @environment.save!
  36 + end
  37 +
  38 + def conditions(table)
  39 + last_run = @environment.settings[:spaminator_last_run]
  40 + if last_run
  41 + ["profiles.environment_id = ? AND #{table}.created_at > ?", @environment.id, last_run]
  42 + else
  43 + [ "profiles.environment_id = ?", @environment.id]
  44 + end
  45 + end
  46 +
  47 + def process_all_comments
  48 + puts 'Processing comments ...'
  49 + i = 0
  50 + comments = Comment.joins("JOIN articles ON (comments.source_id = articles.id AND comments.source_type = 'Article') JOIN profiles ON (profiles.id = articles.profile_id)").where(conditions(:comments))
  51 + total = comments.count
  52 + comments.find_each do |comment|
  53 + puts "Comment #{i += 1}/#{total} (#{100*i/total}%)"
  54 + process_comment(comment)
  55 + end
  56 + end
  57 +
  58 + def process_all_people
  59 + puts 'Processing people ...'
  60 + i = 0
  61 + people = Person.where(conditions(:profiles))
  62 + total = people.count
  63 + people.find_each do |person|
  64 + puts "Person #{i += 1}/#{total} (#{100*i/total}%)"
  65 + process_person(person)
  66 + end
  67 + end
  68 +
  69 + def process_comment(comment)
  70 + comment.check_for_spam
  71 +
  72 + # TODO several comments with the same content:
  73 + # → disable author
  74 + # → mark all of them as spam
  75 +
  76 + # TODO check comments that contains URL's
  77 + end
  78 +
  79 + def process_person(person)
  80 + # person is author of more than 2 comments marked as spam
  81 + # → burn
  82 + #
  83 + number_of_spam_comments = Comment.spam.where(author_id => person.id).count
  84 + if number_of_spam_comments > 2
  85 + mark_as_spammer(person)
  86 + end
  87 + end
  88 +
  89 + def process_people_without_network
  90 + # people who signed up more than one month ago, have no friends and <= 1
  91 + # communities
  92 + #
  93 + # → burn
  94 + # → mark their comments as spam
  95 + #
  96 + Person.where(:environment_id => @environment.id).where(['created_at < ?', Time.now - 1.month]).find_each do |person|
  97 + # TODO progress indicator - see process_all_people above
  98 + number_of_friends = person.friends.count
  99 + number_of_communities = person.communities.count
  100 + if number_of_friends == 0 && number_of_communities <= 1
  101 + mark_as_spammer(person)
  102 + Comment.where(:author_id => person.id).find_each do |comment|
  103 + comment.spam!
  104 + end
  105 + end
  106 + end
  107 + end
  108 +
  109 + def mark_as_spammer(person)
  110 + # FIXME create an AbuseComplaint and finish instead of calling
  111 + # Person#disable directly
  112 + person.disable
  113 + end
  114 +
  115 +end
... ...
plugins/anti_spam/test/unit/anti_spam_plugin/comment_wrapper_test.rb 0 → 100644
... ... @@ -0,0 +1,46 @@
  1 +require 'test_helper'
  2 +
  3 +class AntiSpamPluginCommentWrapperTest < ActiveSupport::TestCase
  4 +
  5 + def setup
  6 + @comment = Comment.new(
  7 + :title => 'comment title',
  8 + :body => 'comment body',
  9 + :name => 'foo',
  10 + :email => 'foo@example.com',
  11 + :ip_address => '1.2.3.4',
  12 + :user_agent => 'Some Good Browser (I hope)',
  13 + :referrer => 'http://noosfero.org/'
  14 + )
  15 + @wrapper = AntiSpamPlugin::CommentWrapper.new(@comment)
  16 + end
  17 +
  18 + should 'use Rakismet::Model' do
  19 + assert_includes @wrapper.class.included_modules, Rakismet::Model
  20 + end
  21 +
  22 + should 'get contents' do
  23 + assert_equal @comment.body, @wrapper.content
  24 + end
  25 +
  26 + should 'get author name' do
  27 + assert_equal @comment.author_name, @wrapper.author
  28 + end
  29 +
  30 + should 'get author email' do
  31 + assert_equal @comment.author_email, @wrapper.author_email
  32 + end
  33 +
  34 + should 'get IP address' do
  35 + assert_equal @comment.ip_address, @wrapper.user_ip
  36 + end
  37 +
  38 + should 'get User-Agent' do
  39 + assert_equal @comment.user_agent, @wrapper.user_agent
  40 + end
  41 +
  42 + should 'get get Referrer' do
  43 + assert_equal @comment.referrer, @wrapper.referrer
  44 + end
  45 +
  46 +end
... ...
plugins/anti_spam/test/unit/anti_spam_plugin/settings_test.rb 0 → 100644
... ... @@ -0,0 +1,29 @@
  1 +require 'test_helper'
  2 +
  3 +class AntiSpamSettingsTest < ActiveSupport::TestCase
  4 +
  5 + def setup
  6 + @environment = Environment.new
  7 + @settings = AntiSpamPlugin::Settings.new(@environment)
  8 + end
  9 +
  10 + should 'store setttings in environment' do
  11 + @settings.host = 'foo.com'
  12 + @settings.api_key = '1234567890'
  13 + assert_equal 'foo.com', @environment.settings[:anti_spam_plugin][:host]
  14 + assert_equal '1234567890', @environment.settings[:anti_spam_plugin][:api_key]
  15 + assert_equal 'foo.com', @settings.host
  16 + assert_equal '1234567890', @settings.api_key
  17 + end
  18 +
  19 + should 'save environment on save' do
  20 + @environment.expects(:save!)
  21 + @settings.save!
  22 + end
  23 +
  24 + should 'use TypePad AntiSpam by default' do
  25 + assert_equal 'api.antispam.typepad.com', @settings.host
  26 + end
  27 +
  28 +
  29 +end
... ...
plugins/anti_spam/test/unit/anti_spam_plugin/spaminator_test.rb 0 → 100644
... ... @@ -0,0 +1,53 @@
  1 +require 'test_helper'
  2 +
  3 +class AntiSpamPluginSpaminatorTest < ActiveSupport::TestCase
  4 +
  5 + def setup
  6 + @environment = Environment.new
  7 + @environment.id = 99
  8 + @spaminator = AntiSpamPlugin::Spaminator.new(@environment)
  9 + @spaminator.stubs(:puts)
  10 + @now = Time.now
  11 + Time.stubs(:now).returns(@now)
  12 + end
  13 +
  14 + should 'search everything in the first run' do
  15 + assert_equal(['profiles.environment_id = ?',99], @spaminator.send(:conditions, nil))
  16 + end
  17 +
  18 + should 'search using recorded last date' do
  19 + @environment.settings[:spaminator_last_run] = @now
  20 + assert_equal(['profiles.environment_id = ? AND table.created_at > ?', 99, @now], @spaminator.send(:conditions, 'table'))
  21 + end
  22 +
  23 + should 'record time of last run in environment' do
  24 + @spaminator.expects(:process_all_comments)
  25 + @spaminator.expects(:process_all_people)
  26 + @environment.stubs(:save!)
  27 + @spaminator.run
  28 + assert_equal @now, @environment.settings[:spaminator_last_run]
  29 + end
  30 +
  31 + should 'find all comments' do
  32 + @spaminator.stubs(:process_comment)
  33 + @spaminator.send :process_all_comments
  34 + end
  35 +
  36 + should 'find all people' do
  37 + @spaminator.stubs(:process_person)
  38 + @spaminator.send :process_all_people
  39 + end
  40 +
  41 + should 'find all comments newer than a date' do
  42 + @environment.settings[:spaminator_last_run] = Time.now - 1.month
  43 + @spaminator.stubs(:process_comment)
  44 + @spaminator.send :process_all_comments
  45 + end
  46 +
  47 + should 'find all people newer than a date' do
  48 + @environment.settings[:spaminator_last_run] = Time.now - 1.month
  49 + @spaminator.stubs(:process_person)
  50 + @spaminator.send :process_all_people
  51 + end
  52 +
  53 +end
... ...
plugins/anti_spam/test/unit/anti_spam_plugin_test.rb 0 → 100644
... ... @@ -0,0 +1,36 @@
  1 +require 'test_helper'
  2 +
  3 +class AntiSpamPluginTest < ActiveSupport::TestCase
  4 +
  5 + def setup
  6 + profile = fast_create(Profile)
  7 + article = fast_create(TextileArticle, :profile_id => profile.id)
  8 + @comment = fast_create(Comment, :source_id => article.id, :source_type => 'Article')
  9 +
  10 + @settings = AntiSpamPlugin::Settings.new(@comment.environment)
  11 + @settings.api_key = 'b8b80ddb8084062d0c9119c945ce3bc3'
  12 + @settings.save!
  13 +
  14 + @plugin = AntiSpamPlugin.new
  15 + @plugin.context = @comment
  16 + end
  17 +
  18 + should 'check for spam and mark comment as spam if server says it is spam' do
  19 + AntiSpamPlugin::CommentWrapper.any_instance.expects(:spam?).returns(true)
  20 + @comment.expects(:save!)
  21 +
  22 + @plugin.check_comment_for_spam(@comment)
  23 + assert @comment.spam
  24 + end
  25 +
  26 + should 'report spam' do
  27 + AntiSpamPlugin::CommentWrapper.any_instance.expects(:spam!)
  28 + @plugin.comment_marked_as_spam(@comment)
  29 + end
  30 +
  31 + should 'report ham' do
  32 + AntiSpamPlugin::CommentWrapper.any_instance.expects(:ham!)
  33 + @plugin.comment_marked_as_ham(@comment)
  34 + end
  35 +
  36 +end
... ...
plugins/anti_spam/views/anti_spam_plugin_admin/index.rhtml 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +<h1><%= _('AntiSpam settings')%></h1>
  2 +
  3 +<% form_for(:settings) do |f| %>
  4 +
  5 + <%= labelled_form_field _('Host'), f.text_field(:host) %>
  6 +
  7 + <%= labelled_form_field _('API key'), f.text_field(:api_key, :size => 40) %>
  8 +
  9 + <% button_bar do %>
  10 + <%= submit_button(:save, _('Save'), :cancel => {:controller => 'plugins', :action => 'index'}) %>
  11 + <% end %>
  12 +
  13 +<% end %>
  14 +
... ...