[noosfero/noosfero][master] 4 commits: Use ~ as placeholder for current user in URLs

Rodrigo Souto gitlab at gitlab.com
Thu May 7 16:48:22 BRT 2015


Rodrigo Souto pushed to branch master at Noosfero / noosfero


Commits:
38e1a327 by David Carlos at 2015-04-27T15:12:47Z
Use ~ as placeholder for current user in URLs

When the :profile parameter is '~', replace it with the identifier of
the currently logged-in user and redirect. This is useful for example
for adding direct links to the control panel of the current user in
documentation.

Signed-off-by: Antonio Terceiro <terceiro at colivre.coop.br>
Signed-off-by: Arthur Del Esposte <arthurmde at gmail.com>
Signed-off-by: David Carlos <ddavidcarlos1392 at gmail.com>
Signed-off-by: Gabriela Navarro <navarro1703 at gmail.com>

- - - - -
b3632c6b by Rodrigo Souto at 2015-05-07T15:43:10Z
Merge branch 'alternative_redirect_behavior' into 'next'

Add alternative redirect's behavior when user is '~'

This is a new feature to create generic URL's related to usernames in Noosfero. It is useful for internal links on articles and for general links on themes.

Examples:
 - If you're logged in as "test" and you go to /~/blog it will redirect you to /test/blog
 - If you're logged in as "test" and you go to /myprofile/~ it will redirect you to /myprofile/test
 - If you're not logged in and you go to /profile/~ or any other link using "~" it will give you a not found page(404).

See merge request !518

- - - - -
cdb34d51 by Arthur Del Esposte at 2015-03-23T15:30:12Z
Replace fixed option to introduce new edit modes for blocks

It resolves issue #38

Signed-off-by: Arthur Del Esposte <arthurmde at gmail.com>
Signed-off-by: David Carlos <ddavidcarlos1392 at gmail.com>
Signed-off-by: Gabriela Navarro <navarro1703 at gmail.com>

- - - - -
30ffafc5 by Rodrigo Souto at 2015-05-07T13:18:05Z
Merge branch 'block_edit_modes' into next

Conflicts:
	test/unit/boxes_helper_test.rb

- - - - -


12 changed files:

- app/controllers/application_controller.rb
- app/controllers/my_profile/profile_design_controller.rb
- app/helpers/boxes_helper.rb
- app/models/block.rb
- app/views/box_organizer/edit.html.erb
- config/routes.rb
- lib/noosfero.rb
- test/functional/application_controller_test.rb
- test/functional/profile_design_controller_test.rb
- test/integration/routing_test.rb
- test/unit/block_test.rb
- test/unit/boxes_helper_test.rb


Changes:

=====================================
app/controllers/application_controller.rb
=====================================
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
   before_filter :allow_cross_domain_access
   before_filter :login_required, :if => :private_environment?
   before_filter :verify_members_whitelist, :if => [:private_environment?, :user]
+  before_filter :redirect_to_current_user
 
   def verify_members_whitelist
     render_access_denied unless user.is_admin? || environment.in_whitelist?(user)
@@ -192,4 +193,15 @@ class ApplicationController < ActionController::Base
   def private_environment?
     @environment.enabled?(:restrict_to_members)
   end
+
+  def redirect_to_current_user
+    if params[:profile] == '~'
+      if logged_in?
+        redirect_to params.merge(:profile => user.identifier)
+      else
+        render_not_found
+      end
+    end
+  end
+
 end


=====================================
app/controllers/my_profile/profile_design_controller.rb
=====================================
--- a/app/controllers/my_profile/profile_design_controller.rb
+++ b/app/controllers/my_profile/profile_design_controller.rb
@@ -4,11 +4,19 @@ class ProfileDesignController < BoxOrganizerController
 
   protect 'edit_profile_design', :profile
 
-  before_filter :protect_fixed_block, :only => [:save, :move_block]
+  before_filter :protect_uneditable_block, :only => [:save]
+  before_filter :protect_fixed_block, :only => [:move_block]
+
+  def protect_uneditable_block
+    block = boxes_holder.blocks.find(params[:id].gsub(/^block-/, ''))
+    if !current_person.is_admin? && !block.editable?
+      render_access_denied
+    end
+  end
 
   def protect_fixed_block
     block = boxes_holder.blocks.find(params[:id].gsub(/^block-/, ''))
-    if block.fixed && !current_person.is_admin?
+    if !current_person.is_admin? && !block.movable?
       render_access_denied
     end
   end


=====================================
app/helpers/boxes_helper.rb
=====================================
--- a/app/helpers/boxes_helper.rb
+++ b/app/helpers/boxes_helper.rb
@@ -190,7 +190,7 @@ module BoxesHelper
       else
         "before-block-#{block.id}"
       end
-    if block.nil? or modifiable?(block)
+    if block.nil? || movable?(block)
       content_tag('div', ' ', :id => id, :class => 'block-target' ) + drop_receiving_element(id, :url => { :action => 'move_block', :target => id }, :accept => box.acceptable_blocks, :hoverclass => 'block-target-hover')
     else
       ""
@@ -199,14 +199,14 @@ module BoxesHelper
 
   # makes the given block draggable so it can be moved away.
   def block_handle(block)
-    modifiable?(block) ? draggable_element("block-#{block.id}", :revert => true) : ""
+    movable?(block) ? draggable_element("block-#{block.id}", :revert => true) : ""
   end
 
   def block_edit_buttons(block)
     buttons = []
     nowhere = 'javascript: return false;'
 
-    if modifiable?(block)
+    if movable?(block)
       if block.first?
         buttons << icon_button('up-disabled', _("Can't move up anymore."), nowhere)
       else
@@ -229,15 +229,15 @@ module BoxesHelper
           buttons << icon_button('left', _('Move to the opposite side'), { :action => 'move_block', :target => 'end-of-box-' + holder.boxes[1].id.to_s, :id => block.id }, :method => 'post' )
         end
       end
+    end
 
-      if block.editable?
-        buttons << modal_icon_button(:edit, _('Edit'), { :action => 'edit', :id => block.id })
-      end
+    if editable?(block)
+      buttons << modal_icon_button(:edit, _('Edit'), { :action => 'edit', :id => block.id })
+    end
 
-      if !block.main?
-        buttons << icon_button(:delete, _('Remove block'), { :action => 'remove', :id => block.id }, { :method => 'post', :confirm => _('Are you sure you want to remove this block?')})
-        buttons << icon_button(:clone, _('Clone'), { :action => 'clone_block', :id => block.id }, { :method => 'post' })
-      end
+    if movable?(block) && !block.main?
+      buttons << icon_button(:delete, _('Remove block'), { :action => 'remove', :id => block.id }, { :method => 'post', :confirm => _('Are you sure you want to remove this block?')})
+      buttons << icon_button(:clone, _('Clone'), { :action => 'clone_block', :id => block.id }, { :method => 'post' })
     end
 
     if block.respond_to?(:help)
@@ -273,7 +273,11 @@ module BoxesHelper
     classes
   end
 
-  def modifiable?(block)
-    return !block.fixed || environment.admins.include?(user)
+  def movable?(block)
+    return block.movable? || user.is_admin?
+  end
+
+  def editable?(block)
+    return block.editable? || user.is_admin?
   end
 end


=====================================
app/models/block.rb
=====================================
--- a/app/models/block.rb
+++ b/app/models/block.rb
@@ -1,6 +1,8 @@
 class Block < ActiveRecord::Base
 
-  attr_accessible :title, :display, :limit, :box_id, :posts_per_page, :visualization_format, :language, :display_user, :box, :fixed
+  attr_accessible :title, :display, :limit, :box_id, :posts_per_page,
+                  :visualization_format, :language, :display_user,
+                  :box, :edit_modes, :move_modes
 
   # to be able to generate HTML
   include ActionView::Helpers::UrlHelper
@@ -110,8 +112,13 @@ class Block < ActiveRecord::Base
   # * <tt>'all'</tt>: the block is always displayed
   settings_items :language, :type => :string, :default => 'all'
 
-  # The block can be configured to be fixed. Only can be edited by environment admins
-  settings_items :fixed, :type => :boolean, :default => false
+  # The block can be configured to define the edition modes options. Only can be edited by environment admins
+  # It can assume the following values:
+  #
+  # * <tt>'all'</tt>: the block owner has all edit options for this block
+  # * <tt>'none'</tt>: the block owner can't do anything with the block
+  settings_items :edit_modes, :type => :string, :default => 'all'
+  settings_items :move_modes, :type => :string, :default => 'all'
 
   # returns the description of the block, used when the user sees a list of
   # blocks to choose one to include in the design.
@@ -148,7 +155,11 @@ class Block < ActiveRecord::Base
 
   # Is this block editable? (Default to <tt>false</tt>)
   def editable?
-    true
+    self.edit_modes == "all"
+  end
+
+  def movable?
+    self.move_modes == "all"
   end
 
   # must always return false, except on MainBlock clas.
@@ -228,6 +239,21 @@ class Block < ActiveRecord::Base
     }
   end
 
+  def edit_block_options
+    @edit_options ||= {
+      'all'            => _('Can be modified'),
+      'none'           => _('Cannot be modified')
+    }
+  end
+
+  def move_block_options
+    @move_options ||= {
+      'all'            => _('Can be moved'),
+      'none'           => _('Cannot be moved')
+    }
+  end
+
+
   def duplicate
     duplicated_block = self.dup
     duplicated_block.display = 'never'


=====================================
app/views/box_organizer/edit.html.erb
=====================================
--- a/app/views/box_organizer/edit.html.erb
+++ b/app/views/box_organizer/edit.html.erb
@@ -5,12 +5,6 @@
 
     <%= labelled_form_field(_('Custom title for this block: '), text_field(:block, :title, :maxlength => 20)) %>
 
-    <% if environment.admins.include?(user) %>
-      <div class="fixed_block">
-        <%= labelled_check_box(_("Fixed"), "block[fixed]", value = "1", checked = @block.fixed) %>
-      </div>
-    <% end %>
-
     <%= render :partial => partial_for_class(@block.class) %>
 
     <div class="display">
@@ -25,6 +19,15 @@
 
     <%= labelled_form_field(_('Show for:'), select(:block, :language, [ [ _('all languages'), 'all']] + environment.locales.map {|key, value| [value, key]} )) %>
 
+    <% if user.is_admin? %>
+      <div class="edit-modes">
+        <%= labelled_form_field _('Edit options:'), select_tag('block[edit_modes]', options_from_collection_for_select(@block.edit_block_options, :first, :last, @block.edit_modes)) %>
+      </div>
+      <div class="move-modes">
+        <%= labelled_form_field _('Move options:'), select_tag('block[move_modes]', options_from_collection_for_select(@block.move_block_options, :first, :last, @block.move_modes)) %>
+      </div>
+    <% end %>
+
     <% button_bar do %>
       <%= submit_button(:save, _('Save')) %>
       <%= modal_close_button(_('Cancel')) %>


=====================================
config/routes.rb
=====================================
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -56,37 +56,37 @@ Noosfero::Application.routes.draw do
   match 'search(/:action(/*category_path))', :controller => 'search'
 
   # events
-  match 'profile/:profile/events_by_day', :controller => 'events', :action => 'events_by_day', :profile => /#{Noosfero.identifier_format}/
-  match 'profile/:profile/events_by_month', :controller => 'events', :action => 'events_by_month', :profile => /#{Noosfero.identifier_format}/
-  match 'profile/:profile/events/:year/:month/:day', :controller => 'events', :action => 'events', :year => /\d*/, :month => /\d*/, :day => /\d*/, :profile => /#{Noosfero.identifier_format}/
-  match 'profile/:profile/events/:year/:month', :controller => 'events', :action => 'events', :year => /\d*/, :month => /\d*/, :profile => /#{Noosfero.identifier_format}/
-  match 'profile/:profile/events', :controller => 'events', :action => 'events', :profile => /#{Noosfero.identifier_format}/
+  match 'profile/:profile/events_by_day', :controller => 'events', :action => 'events_by_day', :profile => /#{Noosfero.identifier_format_in_url}/
+  match 'profile/:profile/events_by_month', :controller => 'events', :action => 'events_by_month', :profile => /#{Noosfero.identifier_format_in_url}/
+  match 'profile/:profile/events/:year/:month/:day', :controller => 'events', :action => 'events', :year => /\d*/, :month => /\d*/, :day => /\d*/, :profile => /#{Noosfero.identifier_format_in_url}/
+  match 'profile/:profile/events/:year/:month', :controller => 'events', :action => 'events', :year => /\d*/, :month => /\d*/, :profile => /#{Noosfero.identifier_format_in_url}/
+  match 'profile/:profile/events', :controller => 'events', :action => 'events', :profile => /#{Noosfero.identifier_format_in_url}/
 
   # catalog
-  match 'catalog/:profile', :controller => 'catalog', :action => 'index', :profile => /#{Noosfero.identifier_format}/, :as => :catalog
+  match 'catalog/:profile', :controller => 'catalog', :action => 'index', :profile => /#{Noosfero.identifier_format_in_url}/, :as => :catalog
 
   # invite
-  match 'profile/:profile/invite/friends', :controller => 'invite', :action => 'invite_friends', :profile => /#{Noosfero.identifier_format}/
-  match 'profile/:profile/invite/:action', :controller => 'invite', :profile => /#{Noosfero.identifier_format}/
+  match 'profile/:profile/invite/friends', :controller => 'invite', :action => 'invite_friends', :profile => /#{Noosfero.identifier_format_in_url}/
+  match 'profile/:profile/invite/:action', :controller => 'invite', :profile => /#{Noosfero.identifier_format_in_url}/
 
   # feeds per tag
-  match 'profile/:profile/tags/:id/feed', :controller => 'profile', :action =>'tag_feed', :id => /.+/, :profile => /#{Noosfero.identifier_format}/, :as => :tag_feed
+  match 'profile/:profile/tags/:id/feed', :controller => 'profile', :action =>'tag_feed', :id => /.+/, :profile => /#{Noosfero.identifier_format_in_url}/, :as => :tag_feed
 
   # profile tags
-  match 'profile/:profile/tags/:id', :controller => 'profile', :action => 'content_tagged', :id => /.+/, :profile => /#{Noosfero.identifier_format}/
-  match 'profile/:profile/tags(/:id)', :controller => 'profile', :action => 'tags', :profile => /#{Noosfero.identifier_format}/
+  match 'profile/:profile/tags/:id', :controller => 'profile', :action => 'content_tagged', :id => /.+/, :profile => /#{Noosfero.identifier_format_in_url}/
+  match 'profile/:profile/tags(/:id)', :controller => 'profile', :action => 'tags', :profile => /#{Noosfero.identifier_format_in_url}/
 
   # profile search
-  match 'profile/:profile/search', :controller => 'profile_search', :action => 'index', :profile => /#{Noosfero.identifier_format}/
+  match 'profile/:profile/search', :controller => 'profile_search', :action => 'index', :profile => /#{Noosfero.identifier_format_in_url}/
 
   # comments
-  match 'profile/:profile/comment/:action/:id', :controller => 'comment', :profile => /#{Noosfero.identifier_format}/
+  match 'profile/:profile/comment/:action/:id', :controller => 'comment', :profile => /#{Noosfero.identifier_format_in_url}/
 
   # public profile information
-  match 'profile/:profile(/:action(/:id))', :controller => 'profile', :action => 'index', :id => /[^\/]*/, :profile => /#{Noosfero.identifier_format}/, :as => :profile
+  match 'profile/:profile(/:action(/:id))', :controller => 'profile', :action => 'index', :id => /[^\/]*/, :profile => /#{Noosfero.identifier_format_in_url}/, :as => :profile
 
   # contact
-  match 'contact/:profile/:action(/:id)', :controller => 'contact', :action => 'index', :id => /.*/, :profile => /#{Noosfero.identifier_format}/
+  match 'contact/:profile/:action(/:id)', :controller => 'contact', :action => 'index', :id => /.*/, :profile => /#{Noosfero.identifier_format_in_url}/
 
   # map balloon
   match 'map_balloon/:action/:id', :controller => 'map_balloon', :id => /.*/
@@ -98,8 +98,8 @@ Noosfero::Application.routes.draw do
   ## Controllers that are profile-specific (for profile admins )
   ######################################################
   # profile customization - "My profile"
-  match 'myprofile/:profile', :controller => 'profile_editor', :action => 'index', :profile => /#{Noosfero.identifier_format}/
-  match 'myprofile/:profile/:controller(/:action(/:id))', :controller => Noosfero.pattern_for_controllers_in_directory('my_profile'), :profile => /#{Noosfero.identifier_format}/, :as => :myprofile
+  match 'myprofile/:profile', :controller => 'profile_editor', :action => 'index', :profile => /#{Noosfero.identifier_format_in_url}/
+  match 'myprofile/:profile/:controller(/:action(/:id))', :controller => Noosfero.pattern_for_controllers_in_directory('my_profile'), :profile => /#{Noosfero.identifier_format_in_url}/, :as => :myprofile
 
 
   ######################################################
@@ -127,14 +127,14 @@ Noosfero::Application.routes.draw do
   # cache stuff - hack
   match 'public/:action/:id', :controller => 'public'
 
-  match ':profile/*page/versions', :controller => 'content_viewer', :action => 'article_versions', :profile => /#{Noosfero.identifier_format}/, :constraints => EnvironmentDomainConstraint.new
+  match ':profile/*page/versions', :controller => 'content_viewer', :action => 'article_versions', :profile => /#{Noosfero.identifier_format_in_url}/, :constraints => EnvironmentDomainConstraint.new
   match '*page/versions', :controller => 'content_viewer', :action => 'article_versions'
 
-  match ':profile/*page/versions_diff', :controller => 'content_viewer', :action => 'versions_diff', :profile => /#{Noosfero.identifier_format}/, :constraints => EnvironmentDomainConstraint.new
+  match ':profile/*page/versions_diff', :controller => 'content_viewer', :action => 'versions_diff', :profile => /#{Noosfero.identifier_format_in_url}/, :constraints => EnvironmentDomainConstraint.new
   match '*page/versions_diff', :controller => 'content_viewer', :action => 'versions_diff'
 
   # match requests for profiles that don't have a custom domain
-  match ':profile(/*page)', :controller => 'content_viewer', :action => 'view_page', :profile => /#{Noosfero.identifier_format}/, :constraints => EnvironmentDomainConstraint.new
+  match ':profile(/*page)', :controller => 'content_viewer', :action => 'view_page', :profile => /#{Noosfero.identifier_format_in_url}/, :constraints => EnvironmentDomainConstraint.new
 
   # match requests for content in domains hosted for profiles
   match '/(*page)', :controller => 'content_viewer', :action => 'view_page'


=====================================
lib/noosfero.rb
=====================================
--- a/lib/noosfero.rb
+++ b/lib/noosfero.rb
@@ -57,6 +57,12 @@ module Noosfero
     '[a-z0-9][a-z0-9~.]*([_\-][a-z0-9~.]+)*'
   end
 
+  # All valid identifiers, plus ~ meaning "the current user". See
+  # ApplicationController#redirect_to_current_user
+  def self.identifier_format_in_url
+    "(#{identifier_format}|~)"
+  end
+
   def self.default_hostname
     Environment.table_exists? && Environment.default ? Environment.default.default_hostname : 'localhost'
   end


=====================================
test/functional/application_controller_test.rb
=====================================
--- a/test/functional/application_controller_test.rb
+++ b/test/functional/application_controller_test.rb
@@ -578,4 +578,22 @@ class ApplicationControllerTest < ActionController::TestCase
     assert_response :success
   end
 
+  should "redirect to 404 if profile is '~' and user is not logged in" do
+    get :index, :profile => '~'
+    assert_response :missing
+  end
+
+  should "redirect to action when profile is '~' " do
+    login_as('ze')
+    get :index, :profile => '~'
+    assert_response 302
+  end
+
+  should "substitute '~' by current user and redirect properly " do
+    login_as('ze')
+    profile = Profile.where(:identifier => 'ze').first
+    get :index, :profile => '~'
+    assert_redirected_to :controller => 'test', :action => 'index', :profile => profile.identifier
+  end
+
 end


=====================================
test/functional/profile_design_controller_test.rb
=====================================
--- a/test/functional/profile_design_controller_test.rb
+++ b/test/functional/profile_design_controller_test.rb
@@ -737,9 +737,9 @@ class ProfileDesignControllerTest < ActionController::TestCase
     end
   end
 
-  test 'should forbid POST to save for fixed blocks' do
+  test 'should forbid POST to save for uneditable blocks' do
     block = profile.blocks.last
-    block.fixed = true
+    block.edit_modes = "none"
     block.save!
 
     post :save, id: block.id, profile: profile.identifier
@@ -748,7 +748,7 @@ class ProfileDesignControllerTest < ActionController::TestCase
 
   test 'should forbid POST to move_block for fixed blocks' do
     block = profile.blocks.last
-    block.fixed = true
+    block.move_modes = "none"
     block.save!
 
     post :move_block, id: block.id, profile: profile.identifier, target: "end-of-box-#{@box3.id}"


=====================================
test/integration/routing_test.rb
=====================================
--- a/test/integration/routing_test.rb
+++ b/test/integration/routing_test.rb
@@ -272,4 +272,8 @@ class RoutingTest < ActionController::IntegrationTest
     assert_routing('/embed/block/12345', :controller => 'embed', :action => 'block', :id => '12345')
   end
 
+  should 'accept ~ as placeholder for current user' do
+    assert_routing('/profile/~', :controller => 'profile', :profile => '~', :action => 'index')
+  end
+
 end


=====================================
test/unit/block_test.rb
=====================================
--- a/test/unit/block_test.rb
+++ b/test/unit/block_test.rb
@@ -35,6 +35,24 @@ class BlockTest < ActiveSupport::TestCase
     assert Block.new.editable?
   end
 
+  should 'be editable if edit modes is all' do
+    block = Block.new
+    block.edit_modes = 'all'
+
+    assert block.editable?
+  end
+
+  should 'be movable by default' do
+    assert Block.new.movable?
+  end
+
+  should 'be movable if move modes is all' do
+    block = Block.new
+    block.move_modes = 'all'
+
+    assert block.movable?
+  end
+
   should 'have default titles' do
     b = Block.new
     b.expects(:default_title).returns('my title')


=====================================
test/unit/boxes_helper_test.rb
=====================================
--- a/test/unit/boxes_helper_test.rb
+++ b/test/unit/boxes_helper_test.rb
@@ -123,24 +123,24 @@ class BoxesHelperTest < ActionView::TestCase
     display_box_content(box, '')
   end
 
-  should 'not show move options on block when block is fixed' do
+  should 'not show move options on block when block has no permission to edit' do
     p = create_user_with_blocks
 
     b = p.blocks.select{|bk| !bk.kind_of?(MainBlock) }[0]
-    b.fixed = true
+    b.move_modes = "none"
     b.save!
 
     stubs(:environment).returns(p.environment)
     stubs(:user).returns(p)
 
-    assert_equal false, modifiable?(b)
+    assert_equal false, movable?(b)
   end
 
-  should 'show move options on block when block is fixed and user is admin' do
+  should 'show move options on block when block has no permission to edit and user is admin' do
     p = create_user_with_blocks
 
     b = p.blocks.select{|bk| !bk.kind_of?(MainBlock) }[0]
-    b.fixed = true
+    b.edit_modes = "none"
     b.save!
 
     p.environment.add_admin(p)
@@ -148,7 +148,7 @@ class BoxesHelperTest < ActionView::TestCase
     stubs(:environment).returns(p.environment)
     stubs(:user).returns(p)
 
-    assert_equal true, modifiable?(b)
+    assert_equal true, movable?(b)
   end
 
   should 'consider boxes_limit without custom_design' do
@@ -198,4 +198,16 @@ class BoxesHelperTest < ActionView::TestCase
     assert_no_tag_in_string block_edit_buttons(block), :tag => 'a', :attributes => {:class => 'button icon-button icon-embed '}
   end
 
+  should 'only show edit option on block' do
+    p = create_user_with_blocks
+
+    b = p.blocks.select{|bk| !bk.kind_of?(MainBlock) }[0]
+    b.edit_modes = "only_edit"
+    b.save!
+
+    stubs(:environment).returns(p.environment)
+    stubs(:user).returns(p)
+
+    assert_equal false, b.editable?
+  end
 end



View it on GitLab: https://gitlab.com/noosfero/noosfero/compare/ee712409e0385611d080e9ee05d8927c3b7676f7...30ffafc5518e52d8757150008e074bde3a8bb23f
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://listas.softwarelivre.org/pipermail/noosfero-dev/attachments/20150507/9d58e00c/attachment-0001.html>


More information about the Noosfero-dev mailing list