Commit e22434f3b45cda07048e320cba3cbcfed1799a10

Authored by Rodrigo Souto
2 parents 427fa1d1 b71625c8

Merge branch 'master' into redemoinho-improvements

Showing 174 changed files with 12846 additions and 7416 deletions   Show diff stats

Too many changes.

To preserve performance only 100 of 174 files displayed.

AUTHORS.md
... ... @@ -90,15 +90,17 @@ Matheus Faria <matheus.sousa.faria@gmail.com>
90 90 Maurilio Atila <cabelotaina@gmail.com>
91 91 M for Momo <mo@rtnp.org>
92 92 Michal Čihař <michal@cihar.com>
93   -Michel Felipe <mfelipeof@gmail.com>
  93 +Michel Felipe de Oliveira Ferreira <michel.ferreira@serpro.gov.br>
94 94 Moises Machado <moises@colivre.coop.br>
95 95 Naíla Alves <naila@colivre.coop.br>
96 96 Nanda Lopes <nanda.listas+psl@gmail.com>
97 97 Niemand Jedermann <predatorix@web.de>
  98 +Omar Junior <omarroinuj@gmail.com>
98 99 Parley Martins <parleypachecomartins@gmail.com>
99 100 Paulo Meirelles <paulo@softwarelivre.org>
100 101 Pedro de Lyra <pedrodelyra@gmail.com>
101 102 Pedro Leal
  103 +Phillip Rohmberger <rohmberger@hotmail.de>
102 104 Rafael de Souza Queiroz <querafael@live.com>
103 105 Rafael Gomes <rafaelgomes@techfree.com.br>
104 106 Rafael Martins <rmmartins@gmail.com>
... ... @@ -112,6 +114,7 @@ Rodrigo Medeiros &lt;rodrigo.mss01@gmail.com&gt;
112 114 Rodrigo Souto <rodrigo@colivre.coop.br>
113 115 Ronny Kursawe <kursawe.ronny@googlemail.com>
114 116 Samuel R. C. Vale <srcvale@holoscopio.com>
  117 +Simiao Carvalho <simiaosimis@gmail.com>
115 118 Tallys Martins <tallysmartins@yahoo.com.br>
116 119 Thiago Casotti <thiago.casotti@uol.com.br>
117 120 Thiago Kairala <thiagor.kairala@gmail.com>
... ... @@ -123,6 +126,7 @@ Valessio Brito &lt;contato@valessiobrito.com.br&gt;
123 126 Victor Costa <vfcosta@gmail.com>
124 127 Victor Hugo Alves de Carvalho <victorhugodf.ac@gmail.com>
125 128 Vinicius Cubas Brand <viniciuscb@gmail.com>
  129 +Vitor Barbosa <vitornga15@gmail.com>
126 130 Wilton Rodrigues <braynwilton@gmail.com>
127 131 Yann Lugrin <yann.lugrin@liquid-concept.ch>
128 132  
... ...
Gemfile
... ... @@ -10,7 +10,7 @@ gem &#39;RedCloth&#39;, &#39;~&gt; 4.2.9&#39;
10 10 gem 'will_paginate', '~> 3.0.3'
11 11 gem 'ruby-feedparser', '~> 0.7'
12 12 gem 'daemons', '~> 1.1.5'
13   -gem 'thin', '~> 1.3.1'
  13 +gem 'unicorn', '~> 4.8'
14 14 gem 'nokogiri', '~> 1.5.5'
15 15 gem 'rake', :require => false
16 16 gem 'rest-client', '~> 1.6.7'
... ... @@ -21,6 +21,13 @@ gem &#39;whenever&#39;, :require =&gt; false
21 21 gem 'eita-jrails', '~> 0.9.5', require: 'jrails'
22 22 gem 'slim'
23 23  
  24 +# API dependencies
  25 +gem 'grape', '~> 0.12'
  26 +gem 'grape-entity'
  27 +gem 'grape_logging'
  28 +gem 'rack-cors'
  29 +gem 'rack-contrib'
  30 +
24 31 # asset pipeline
25 32 gem 'uglifier', '>= 1.0.3'
26 33 gem 'sass-rails'
... ...
Rakefile
... ... @@ -15,4 +15,38 @@ Noosfero::Application.load_tasks
15 15 Dir.glob(pattern).sort
16 16 end.flatten.each do |taskfile|
17 17 load taskfile
  18 +end
  19 +
  20 +# plugins' tasks
  21 +plugins_tasks = Dir.glob("config/plugins/*/{tasks,lib/tasks,rails/tasks}/**/*.rake").sort +
  22 + Dir.glob("config/plugins/*/vendor/plugins/*/{tasks,lib/tasks,rails/tasks}/**/*.rake").sort
  23 +plugins_tasks.each{ |ext| load ext }
  24 +
  25 +
  26 +desc "Print out grape routes"
  27 +task :grape_routes => :environment do
  28 + #require 'api/api.rb'
  29 + Noosfero::API::API.routes.each do |route|
  30 + puts route
  31 + method = route.route_method
  32 + path = route.route_path
  33 + puts " #{method} #{path}"
  34 + end
  35 +end
  36 +
  37 +# plugins' tasks
  38 +plugins_tasks = Dir.glob("config/plugins/*/{tasks,lib/tasks,rails/tasks}/**/*.rake").sort +
  39 + Dir.glob("config/plugins/*/vendor/plugins/*/{tasks,lib/tasks,rails/tasks}/**/*.rake").sort
  40 +plugins_tasks.each{ |ext| load ext }
  41 +
  42 +
  43 +desc "Print out grape routes"
  44 +task :grape_routes => :environment do
  45 + #require 'api/api.rb'
  46 + Noosfero::API::API.routes.each do |route|
  47 + puts route
  48 + method = route.route_method
  49 + path = route.route_path
  50 + puts " #{method} #{path}"
  51 + end
18 52 end
... ...
app/controllers/my_profile/cms_controller.rb
... ... @@ -27,20 +27,13 @@ class CmsController &lt; MyProfileController
27 27  
28 28 helper_method :file_types
29 29  
30   - protect_if :only => :upload_files do |c, user, profile|
31   - article_id = c.params[:parent_id]
32   - (!article_id.blank? && profile.articles.find(article_id).allow_create?(user)) ||
33   - (user && (user.has_permission?('post_content', profile) || user.has_permission?('publish_content', profile)))
34   - end
35   -
36   - protect_if :except => [:suggest_an_article, :set_home_page, :edit, :destroy, :publish, :publish_on_portal_community, :publish_on_communities, :search_communities_to_publish, :upload_files, :new] do |c, user, profile|
  30 + protect_if :except => [:suggest_an_article, :set_home_page, :edit, :destroy, :publish, :upload_files, :new] do |c, user, profile|
37 31 user && (user.has_permission?('post_content', profile) || user.has_permission?('publish_content', profile))
38 32 end
39 33  
40   - protect_if :only => :new do |c, user, profile|
41   - article = profile.articles.find_by_id(c.params[:parent_id])
42   - (!article.nil? && (article.allow_create?(user) || article.parent.allow_create?(user))) ||
43   - (user && (user.has_permission?('post_content', profile) || user.has_permission?('publish_content', profile)))
  34 + protect_if :only => [:new, :upload_files] do |c, user, profile|
  35 + parent = profile.articles.find_by_id(c.params[:parent_id])
  36 + user && user.can_post_content?(profile, parent)
44 37 end
45 38  
46 39 protect_if :only => :destroy do |c, user, profile|
... ... @@ -118,10 +111,7 @@ class CmsController &lt; MyProfileController
118 111 end
119 112 end
120 113  
121   - unless @article.kind_of?(RssFeed)
122   - @escaped_body = CGI::escapeHTML(@article.body || '')
123   - @escaped_abstract = CGI::escapeHTML(@article.abstract || '')
124   - end
  114 + escape_fields @article
125 115 end
126 116  
127 117 def new
... ... @@ -192,6 +182,8 @@ class CmsController &lt; MyProfileController
192 182 end
193 183 end
194 184  
  185 + escape_fields @article
  186 +
195 187 render :action => 'edit'
196 188 end
197 189  
... ... @@ -541,4 +533,10 @@ class CmsController &lt; MyProfileController
541 533 end
542 534 end
543 535  
  536 + def escape_fields article
  537 + unless article.kind_of?(RssFeed)
  538 + @escaped_body = CGI::escapeHTML(article.body || '')
  539 + @escaped_abstract = CGI::escapeHTML(article.abstract || '')
  540 + end
  541 + end
544 542 end
... ...
app/controllers/my_profile/profile_editor_controller.rb
... ... @@ -133,6 +133,13 @@ class ProfileEditorController &lt; MyProfileController
133 133 redirect_to_previous_location
134 134 end
135 135  
  136 + def reset_private_token
  137 + profile = environment.profiles.find(params[:id])
  138 + profile.user.generate_private_token!
  139 +
  140 + redirect_to_previous_location
  141 + end
  142 +
136 143 protected
137 144  
138 145 def redirect_to_previous_location
... ...
app/controllers/public/account_controller.rb
... ... @@ -97,12 +97,9 @@ class AccountController &lt; ApplicationController
97 97 @block_bot = !!session[:may_be_a_bot]
98 98 @invitation_code = params[:invitation_code]
99 99 begin
100   - @user = User.new(params[:user])
  100 + @user = User.build(params[:user], params[:profile_data], environment)
101 101 @user.session = session
102   - @user.terms_of_use = environment.terms_of_use
103   - @user.environment = environment
104 102 @terms_of_use = environment.terms_of_use
105   - @user.person_data = params[:profile_data]
106 103 @user.return_to = session[:return_to]
107 104 @person = Person.new(params[:profile_data])
108 105 @person.environment = @user.environment
... ...
app/controllers/public/api_controller.rb 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +class ApiController < PublicController
  2 +
  3 + no_design_blocks
  4 +
  5 + helper_method :endpoints
  6 +
  7 + def index
  8 + end
  9 +
  10 + def playground
  11 + end
  12 +
  13 + private
  14 +
  15 + def endpoints
  16 + Noosfero::API::API.endpoints(environment)
  17 + end
  18 +
  19 +end
... ...
app/controllers/public/content_viewer_controller.rb
... ... @@ -8,6 +8,7 @@ class ContentViewerController &lt; ApplicationController
8 8 helper TagsHelper
9 9  
10 10 def view_page
  11 +
11 12 path = get_path(params[:page], params[:format])
12 13  
13 14 @version = params[:version].to_i
... ... @@ -38,7 +39,7 @@ class ContentViewerController &lt; ApplicationController
38 39 end
39 40  
40 41 # At this point the page will be showed
41   - @page.hit unless user_is_a_bot?
  42 + @page.hit unless user_is_a_bot? || already_visited?(@page)
42 43  
43 44 @page = FilePresenter.for @page
44 45  
... ... @@ -272,4 +273,18 @@ class ContentViewerController &lt; ApplicationController
272 273 @comment_order = params[:comment_order].nil? ? 'oldest' : params[:comment_order]
273 274 end
274 275  
  276 + private
  277 +
  278 + def already_visited?(element)
  279 + user_id = if user.nil? then -1 else current_user.id end
  280 + user_id = "#{user_id}_#{element.id}_#{element.class}"
  281 +
  282 + if cookies.signed[:visited] == user_id
  283 + return true
  284 + else
  285 + cookies.permanent.signed[:visited] = user_id
  286 + return false
  287 + end
  288 + end
  289 +
275 290 end
... ...
app/controllers/public/profile_controller.rb
... ... @@ -66,7 +66,10 @@ class ProfileController &lt; PublicController
66 66  
67 67 def members
68 68 if is_cache_expired?(profile.members_cache_key(params))
69   - @members = profile.members_by_name.includes(relations_to_include).paginate(:per_page => members_per_page, :page => params[:npage], :total_entries => profile.members.count)
  69 + sort = (params[:sort] == 'desc') ? params[:sort] : 'asc'
  70 + @profile_admins = profile.admins.includes(relations_to_include).order("name #{sort}").paginate(:per_page => members_per_page, :page => params[:npage])
  71 + @profile_members = profile.members.includes(relations_to_include).order("name #{sort}").paginate(:per_page => members_per_page, :page => params[:npage])
  72 + @profile_members_url = url_for(:controller => 'profile', :action => 'members')
70 73 end
71 74 end
72 75  
... ...
app/helpers/article_helper.rb
... ... @@ -88,7 +88,7 @@ module ArticleHelper
88 88 content_tag( 'small', _('Who will be able to create new topics on this forum?')) +
89 89 content_tag('div', '', slider_options) +
90 90 hidden_field_tag('article[topic_creation]', article.topic_creation) +
91   - javascript_include_tag('topic-creation-config')
  91 + javascript_include_tag("#{Noosfero.root}/assets/topic-creation-config.js")
92 92 end
93 93  
94 94 def privacity_exceptions(article, tokenized_children)
... ...
app/models/add_friend.rb
... ... @@ -14,6 +14,9 @@ class AddFriend &lt; Task
14 14 alias :friend :target
15 15 alias :friend= :target=
16 16  
  17 + validates :requestor, :kind_of => { :kind => Person }
  18 + validates :target, :kind_of => { :kind => Person }
  19 +
17 20 after_create do |task|
18 21 TaskMailer.invitation_notification(task).deliver unless task.friend
19 22 remove_from_suggestion_list(task)
... ...
app/models/add_member.rb
... ... @@ -2,6 +2,9 @@ class AddMember &lt; Task
2 2  
3 3 validates_presence_of :requestor_id, :target_id
4 4  
  5 + validates :requestor, kind_of: {kind: Person}
  6 + validates :target, kind_of: {kind: Organization}
  7 +
5 8 alias :person :requestor
6 9 alias :person= :requestor=
7 10  
... ...
app/models/approve_article.rb
1 1 class ApproveArticle < Task
2 2 validates_presence_of :requestor_id, :target_id
3 3  
  4 + validates :requestor, kind_of: {kind: Person}
  5 + validate :allowed_requestor
  6 +
  7 + def allowed_requestor
  8 + if target
  9 + if target.person? && requestor != target
  10 + self.errors.add(:requestor, _('You can not post articles to other users.'))
  11 + end
  12 + if target.organization? && !target.members.include?(requestor) && target.environment.portal_community != target
  13 + self.errors.add(:requestor, _('Only members can post articles on communities.'))
  14 + end
  15 + end
  16 + end
  17 +
4 18 def article_title
5 19 article ? article.title : _('(The original text was removed)')
6 20 end
7   -
  21 +
8 22 def article
9 23 Article.find_by_id data[:article_id]
10 24 end
... ... @@ -128,4 +142,9 @@ class ApproveArticle &lt; Task
128 142 message
129 143 end
130 144  
  145 + def request_is_member_of_target
  146 + unless requestor.is_member_of?(target)
  147 + errors.add(:approve_article, N_('Requestor must be a member of target.'))
  148 + end
  149 + end
131 150 end
... ...
app/models/article.rb
... ... @@ -130,6 +130,8 @@ class Article &lt; ActiveRecord::Base
130 130 {:include => 'categories_including_virtual', :conditions => { 'categories.id' => category.id }}
131 131 }
132 132  
  133 + include TimeScopes
  134 +
133 135 scope :by_range, lambda { |range| {
134 136 :conditions => [
135 137 'articles.published_at BETWEEN :start_date AND :end_date', { :start_date => range.first, :end_date => range.last }
... ... @@ -627,6 +629,11 @@ class Article &lt; ActiveRecord::Base
627 629 self.hits += 1
628 630 end
629 631  
  632 + def self.hit(articles)
  633 + Article.where(:id => articles.map(&:id)).update_all('hits = hits + 1')
  634 + articles.each { |a| a.hits += 1 }
  635 + end
  636 +
630 637 def can_display_hits?
631 638 true
632 639 end
... ... @@ -718,6 +725,11 @@ class Article &lt; ActiveRecord::Base
718 725 person ? person.id : nil
719 726 end
720 727  
  728 + #FIXME make this test
  729 + def author_custom_image(size = :icon)
  730 + author ? author.profile_custom_image(size) : nil
  731 + end
  732 +
721 733 def version_license(version_number = nil)
722 734 return license if version_number.nil?
723 735 profile.environment.licenses.find_by_id(get_version(version_number).license_id)
... ...
app/models/change_password.rb
... ... @@ -18,6 +18,8 @@ class ChangePassword &lt; Task
18 18  
19 19 validates_presence_of :requestor
20 20  
  21 + validates :requestor, kind_of: {kind: Person}
  22 +
21 23 ###################################################
22 24 # validations for updating a ChangePassword task
23 25  
... ...
app/models/comment.rb
... ... @@ -20,6 +20,8 @@ class Comment &lt; ActiveRecord::Base
20 20  
21 21 scope :without_reply, :conditions => ['reply_of_id IS NULL']
22 22  
  23 + include TimeScopes
  24 +
23 25 # unauthenticated authors:
24 26 validates_presence_of :name, :if => (lambda { |record| !record.email.blank? })
25 27 validates_presence_of :email, :if => (lambda { |record| !record.name.blank? })
... ... @@ -67,6 +69,11 @@ class Comment &lt; ActiveRecord::Base
67 69 author ? author.url : nil
68 70 end
69 71  
  72 + #FIXME make this test
  73 + def author_custom_image(size = :icon)
  74 + author ? author.profile_custom_image(size) : nil
  75 + end
  76 +
70 77 def url
71 78 article.view_url.merge(:anchor => anchor)
72 79 end
... ...
app/models/create_community.rb
... ... @@ -3,6 +3,9 @@ class CreateCommunity &lt; Task
3 3 validates_presence_of :requestor_id, :target_id
4 4 validates_presence_of :name
5 5  
  6 + validates :requestor, kind_of: {kind: Person}
  7 + validates :target, kind_of: {kind: Environment}
  8 +
6 9 alias :environment :target
7 10 alias :environment= :target=
8 11  
... ...
app/models/create_enterprise.rb
... ... @@ -27,6 +27,8 @@ class CreateEnterprise &lt; Task
27 27 # checks for actual attributes
28 28 validates_presence_of :requestor_id, :target_id
29 29  
  30 + validates :requestor, kind_of: {kind: Person}
  31 +
30 32 # checks for admins required attributes
31 33 DATA_FIELDS.each do |attribute|
32 34 validates_presence_of attribute, :if => lambda { |obj| obj.environment.required_enterprise_fields.include?(attribute) }
... ...
app/models/email_activation.rb
1 1 class EmailActivation < Task
2 2  
3 3 validates_presence_of :requestor_id, :target_id
  4 +
  5 + validates :requestor, kind_of: {kind: Person}
  6 + validates :target, kind_of: {kind: Environment}
  7 +
4 8 validate :already_requested, :on => :create
5 9  
6 10 alias :environment :target
7 11 alias :person :requestor
8 12  
9 13 def already_requested
10   - if !self.requestor.nil? && self.requestor.user.email_activation_pending?
  14 + if !self.requestor.nil? && self.requestor.person? && self.requestor.user.email_activation_pending?
11 15 self.errors.add(:base, _('You have already requested activation of your mailbox.'))
12 16 end
13 17 end
... ...
app/models/enterprise_activation.rb
... ... @@ -8,6 +8,8 @@ class EnterpriseActivation &lt; Task
8 8  
9 9 validates_presence_of :enterprise
10 10  
  11 + validates :target, kind_of: {kind: Enterprise}
  12 +
11 13 def perform
12 14 self.enterprise.enable self.requestor
13 15 end
... ...
app/models/invitation.rb
... ... @@ -6,6 +6,8 @@ class Invitation &lt; Task
6 6  
7 7 validates_presence_of :target_id, :if => Proc.new{|invite| invite.friend_email.blank?}
8 8  
  9 + validates :requestor, kind_of: {kind: Person}
  10 +
9 11 validates_presence_of :friend_email, :if => Proc.new{|invite| invite.target_id.blank?}
10 12 validates_format_of :friend_email, :with => Noosfero::Constants::EMAIL_FORMAT, :if => Proc.new{|invite| invite.target_id.blank?}
11 13  
... ... @@ -34,7 +36,7 @@ class Invitation &lt; Task
34 36 end
35 37  
36 38 def not_invite_yourself
37   - email = friend ? friend.user.email : friend_email
  39 + email = friend && friend.person? ? friend.user.email : friend_email
38 40 if person && email && person.user.email == email
39 41 self.errors.add(:base, _("You can't invite youself"))
40 42 end
... ... @@ -136,7 +138,11 @@ class Invitation &lt; Task
136 138 end
137 139  
138 140 def environment
139   - self.requestor.environment
  141 + if self.requestor
  142 + self.requestor.environment
  143 + else
  144 + nil
  145 + end
140 146 end
141 147  
142 148 end
... ...
app/models/moderate_user_registration.rb
... ... @@ -7,6 +7,8 @@ class ModerateUserRegistration &lt; Task
7 7  
8 8 after_create :schedule_spam_checking
9 9  
  10 + validates :target, kind_of: {kind: Environment}
  11 +
10 12 alias :environment :target
11 13 alias :environment= :target=
12 14  
... ...
app/models/organization.rb
... ... @@ -8,6 +8,30 @@ class Organization &lt; Profile
8 8 :display => %w[compact]
9 9 }
10 10  
  11 + # An Organization is considered visible to a given person if one of the
  12 + # following conditions are met:
  13 + # 1) The user is an environment administrator.
  14 + # 2) The user is an administrator of the organization.
  15 + # 3) The user is a member of the organization and the organization is
  16 + # visible.
  17 + # 4) The user is not a member of the organization but the organization is
  18 + # visible, public and enabled.
  19 + def self.visible_for_person(person)
  20 + joins('LEFT JOIN "role_assignments" ON ("role_assignments"."resource_id" = "profiles"."id"
  21 + AND "role_assignments"."resource_type" = \'Profile\') OR (
  22 + "role_assignments"."resource_id" = "profiles"."environment_id" AND
  23 + "role_assignments"."resource_type" = \'Environment\' )')
  24 + .joins('LEFT JOIN "roles" ON "role_assignments"."role_id" = "roles"."id"')
  25 + .where(
  26 + ['( (roles.key = ? OR roles.key = ?) AND role_assignments.accessor_type = ? AND role_assignments.accessor_id = ? )
  27 + OR
  28 + ( ( ( role_assignments.accessor_type = ? AND role_assignments.accessor_id = ? ) OR
  29 + ( profiles.public_profile = ? AND profiles.enabled = ? ) ) AND
  30 + ( profiles.visible = ? ) )',
  31 + 'profile_admin', 'environment_administrator', Profile.name, person.id,
  32 + Profile.name, person.id, true, true, true]
  33 + ).uniq
  34 + end
11 35  
12 36 settings_items :closed, :type => :boolean, :default => false
13 37 def closed?
... ...
app/models/person.rb
... ... @@ -39,6 +39,19 @@ roles] }
39 39 { :select => 'DISTINCT profiles.*', :conditions => ['"profiles"."id" NOT IN (SELECT DISTINCT profiles.id FROM "profiles" INNER JOIN "friendships" ON "friendships"."person_id" = "profiles"."id" WHERE "friendships"."friend_id" IN (%s))' % resources.map(&:id)] }
40 40 }
41 41  
  42 + scope :visible_for_person, lambda { |person|
  43 + joins('LEFT JOIN "role_assignments" ON
  44 + "role_assignments"."resource_id" = "profiles"."environment_id" AND
  45 + "role_assignments"."resource_type" = \'Environment\'')
  46 + .joins('LEFT JOIN "roles" ON "role_assignments"."role_id" = "roles"."id"')
  47 + .joins('LEFT JOIN "friendships" ON "friendships"."friend_id" = "profiles"."id"')
  48 + .where(
  49 + ['( roles.key = ? AND role_assignments.accessor_type = ? AND role_assignments.accessor_id = ? ) OR (
  50 + ( ( friendships.person_id = ? ) OR (profiles.public_profile = ?)) AND (profiles.visible = ?) )', 'environment_administrator', Profile.name, person.id, person.id, true, true]
  51 + ).uniq
  52 + }
  53 +
  54 +
42 55 def has_permission_with_admin?(permission, resource)
43 56 return true if resource.blank? || resource.admins.include?(self)
44 57 return true if resource.kind_of?(Profile) && resource.environment.admins.include?(self)
... ... @@ -128,6 +141,11 @@ roles] }
128 141 self.tracked_notifications.exists?(activity)
129 142 end
130 143  
  144 + def can_post_content?(profile, parent=nil)
  145 + (!parent.nil? && (parent.allow_create?(self))) ||
  146 + (self.has_permission?('post_content', profile) || self.has_permission?('publish_content', profile))
  147 + end
  148 +
131 149 # Sets the identifier for this person. Raises an exception when called on a
132 150 # existing person (since peoples' identifiers cannot be changed)
133 151 def identifier=(value)
... ...
app/models/product.rb
... ... @@ -56,6 +56,25 @@ class Product &lt; ActiveRecord::Base
56 56 {:joins => :product_category, :conditions => ['categories.path LIKE ?', "%#{category.slug}%"]} if category
57 57 }
58 58  
  59 + scope :visible_for_person, lambda { |person|
  60 + joins('INNER JOIN "profiles" enterprises ON enterprises."id" = "products"."profile_id"')
  61 + .joins('LEFT JOIN "role_assignments" ON ("role_assignments"."resource_id" = enterprises."id"
  62 + AND "role_assignments"."resource_type" = \'Profile\') OR (
  63 + "role_assignments"."resource_id" = enterprises."environment_id" AND
  64 + "role_assignments"."resource_type" = \'Environment\' )')
  65 + .joins('LEFT JOIN "roles" ON "role_assignments"."role_id" = "roles"."id"')
  66 + .where(
  67 + ['( (roles.key = ? OR roles.key = ?) AND role_assignments.accessor_type = \'Profile\' AND role_assignments.accessor_id = ? )
  68 + OR
  69 + ( ( ( role_assignments.accessor_type = \'Profile\' AND
  70 + role_assignments.accessor_id = ? ) OR
  71 + ( enterprises.public_profile = ? AND enterprises.enabled = ? ) ) AND
  72 + ( enterprises.visible = ? ) )',
  73 + 'profile_admin', 'environment_administrator', person.id, person.id,
  74 + true, true, true]
  75 + ).uniq
  76 + }
  77 +
59 78 after_update :save_image
60 79  
61 80 def lat
... ...
app/models/profile.rb
... ... @@ -102,6 +102,50 @@ class Profile &lt; ActiveRecord::Base
102 102 }
103 103 scope :no_templates, {:conditions => {:is_template => false}}
104 104  
  105 + # Returns a scoped object to select profiles in a given location or in a radius
  106 + # distance from the given location center.
  107 + # The parameter can be the `request.params` with the keys:
  108 + # * `country`: Country code string.
  109 + # * `state`: Second-level administrative country subdivisions.
  110 + # * `city`: City full name for center definition, or as set by users.
  111 + # * `lat`: The latitude to define the center of georef search.
  112 + # * `lng`: The longitude to define the center of georef search.
  113 + # * `distance`: Define the search radius in kilometers.
  114 + # NOTE: This method may return an exception object, to inform filter error.
  115 + # When chaining scopes, is hardly recommended you to add this as the last one,
  116 + # if you can't be sure about the provided parameters.
  117 + def self.by_location(params)
  118 + params = params.with_indifferent_access
  119 + if params[:distance].blank?
  120 + where_code = []
  121 + [ :city, :state, :country ].each do |place|
  122 + unless params[place].blank?
  123 + # ... So we must to find on this named location
  124 + # TODO: convert location attrs to a table collumn
  125 + where_code << "(profiles.data like '%#{place}: #{params[place]}%')"
  126 + end
  127 + end
  128 + self.where where_code.join(' AND ')
  129 + else # Filter in a georef circle
  130 + unless params[:lat].blank? && params[:lng].blank?
  131 + lat, lng = [ params[:lat].to_f, params[:lng].to_f ]
  132 + end
  133 + if !lat
  134 + location = [ params[:city], params[:state], params[:country] ].compact.join(', ')
  135 + if location.blank?
  136 + return Exception.new (
  137 + _('You must to provide `lat` and `lng`, or `city` and `country` to define the center of the search circle, defined by `distance`.')
  138 + )
  139 + end
  140 + lat, lng = Noosfero::GeoRef.location_to_georef location
  141 + end
  142 + dist = params[:distance].to_f
  143 + self.where "#{Noosfero::GeoRef.sql_dist lat, lng} <= #{dist}"
  144 + end
  145 + end
  146 +
  147 + include TimeScopes
  148 +
105 149 def members
106 150 scopes = plugins.dispatch_scopes(:organization_members, self)
107 151 scopes << Person.members_of(self)
... ... @@ -913,7 +957,8 @@ private :generate_url, :url_options
913 957  
914 958 def members_cache_key(params = {})
915 959 page = params[:npage] || '1'
916   - cache_key + '-members-page-' + page
  960 + sort = (params[:sort] == 'desc') ? params[:sort] : 'asc'
  961 + cache_key + '-members-page-' + page + '-' + sort
917 962 end
918 963  
919 964 def more_recent_label
... ... @@ -951,6 +996,13 @@ private :generate_url, :url_options
951 996 image.public_filename(:icon) if image.present?
952 997 end
953 998  
  999 + #FIXME make this test
  1000 + def profile_custom_image(size = :icon)
  1001 + image_path = profile_custom_icon if size == :icon
  1002 + image_path ||= image.public_filename(size) if image.present?
  1003 + image_path
  1004 + end
  1005 +
954 1006 def jid(options = {})
955 1007 domain = options[:domain] || environment.default_hostname
956 1008 "#{identifier}@#{domain}"
... ...
app/models/profile_activity.rb
... ... @@ -16,6 +16,7 @@ class ProfileActivity &lt; ActiveRecord::Base
16 16  
17 17 def self.update_activity activity
18 18 profile_activity = ProfileActivity.where(activity_id: activity.id, activity_type: activity.class.base_class.name).first
  19 + return unless profile_activity
19 20 profile_activity.send :copy_timestamps
20 21 profile_activity.save!
21 22 profile_activity
... ...
app/models/qualifier.rb
... ... @@ -11,6 +11,12 @@ class Qualifier &lt; ActiveRecord::Base
11 11 has_many :qualifier_certifiers, :dependent => :destroy
12 12 has_many :certifiers, :through => :qualifier_certifiers
13 13  
  14 + def used_certs
  15 + Certifier.joins('INNER JOIN product_qualifiers' +
  16 + ' ON certifiers.id = product_qualifiers.certifier_id')
  17 + .where(product_qualifiers: {qualifier_id: self.id})
  18 + end
  19 +
14 20 has_many :product_qualifiers, :dependent => :destroy
15 21 has_many :products, :through => :product_qualifiers, :source => :product
16 22  
... ...
app/models/scrap.rb
... ... @@ -22,11 +22,11 @@ class Scrap &lt; ActiveRecord::Base
22 22  
23 23 scope :not_replies, :conditions => {:scrap_id => nil}
24 24  
25   - track_actions :leave_scrap, :after_create, :keep_params => ['sender.name', 'content', 'receiver.name', 'receiver.url'], :if => Proc.new{|s| s.sender != s.receiver && s.sender != s.top_root.receiver}, :custom_target => :action_tracker_target
  25 + track_actions :leave_scrap, :after_create, :keep_params => ['sender.name', 'content', 'receiver.name', 'receiver.url'], :if => Proc.new{|s| s.sender != s.receiver && s.sender != s.top_root.receiver}, :custom_target => :action_tracker_target, :custom_user => :sender
26 26  
27   - track_actions :leave_scrap_to_self, :after_create, :keep_params => ['sender.name', 'content'], :if => Proc.new{|s| s.sender == s.receiver}
  27 + track_actions :leave_scrap_to_self, :after_create, :keep_params => ['sender.name', 'content'], :if => Proc.new{|s| s.sender == s.receiver}, :custom_user => :sender
28 28  
29   - track_actions :reply_scrap_on_self, :after_create, :keep_params => ['sender.name', 'content'], :if => Proc.new{|s| s.sender != s.receiver && s.sender == s.top_root.receiver}
  29 + track_actions :reply_scrap_on_self, :after_create, :keep_params => ['sender.name', 'content'], :if => Proc.new{|s| s.sender != s.receiver && s.sender == s.top_root.receiver}, :custom_user => :sender
30 30  
31 31 after_create :send_notification
32 32  
... ...
app/models/suggest_article.rb
... ... @@ -41,6 +41,7 @@ class SuggestArticle &lt; Task
41 41 return type if type < Article
42 42 end
43 43 TinyMceArticle
  44 + (article[:type] || 'TinyMceArticle').constantize
44 45 end
45 46  
46 47 def perform
... ... @@ -91,4 +92,5 @@ class SuggestArticle &lt; Task
91 92 def after_ham!
92 93 self.delay.marked_as_ham
93 94 end
  95 +
94 96 end
... ...
app/models/task.rb
... ... @@ -116,6 +116,51 @@ class Task &lt; ActiveRecord::Base
116 116 end
117 117 end
118 118  
  119 + class KindOfValidator < ActiveModel::EachValidator
  120 + def validate_each(record, attribute, value)
  121 + environment = record.environment || Environment.default
  122 + klass = options[:kind]
  123 + group = klass.to_s.downcase.pluralize
  124 + id = attribute.to_s + "_id"
  125 + if environment.respond_to?(group)
  126 + attrb = value || environment.send(group).find_by_id(record.send(id))
  127 + else
  128 + attrb = value || klass.find_by_id(record.send(id))
  129 + end
  130 + if attrb.respond_to?(klass.to_s.downcase + "?")
  131 + unless attrb.send(klass.to_s.downcase + "?")
  132 + record.errors[attribute] << (options[:message] || "should be "+ klass.to_s.downcase)
  133 + end
  134 + else
  135 + unless attrb.class == klass
  136 + record.errors[attribute] << (options[:message] || "should be "+ klass.to_s.downcase)
  137 + end
  138 + end
  139 + end
  140 + end
  141 +
  142 + def requestor_is_of_kind(klass, message = nil)
  143 + error_message = message ||= _('Task requestor must be '+klass.to_s.downcase)
  144 + group = klass.to_s.downcase.pluralize
  145 + if environment.respond_to?(group) and requestor_id
  146 + requestor = requestor ||= environment.send(klass.to_s.downcase.pluralize).find_by_id(requestor_id)
  147 + end
  148 + unless requestor.class == klass
  149 + errors.add(error_message)
  150 + end
  151 + end
  152 +
  153 + def target_is_of_kind(klass, message = nil)
  154 + error_message = message ||= _('Task target must be '+klass.to_s.downcase)
  155 + group = klass.to_s.downcase.pluralize
  156 + if environment.respond_to?(group) and target_id
  157 + target = target ||= environment.send(klass.to_s.downcase.pluralize).find_by_id(target_id)
  158 + end
  159 + unless target.class == klass
  160 + errors.add(error_message)
  161 + end
  162 + end
  163 +
119 164 def close(status, closed_by)
120 165 self.status = status
121 166 self.end_date = Time.now
... ... @@ -269,6 +314,19 @@ class Task &lt; ActiveRecord::Base
269 314  
270 315 include Spammable
271 316  
  317 + #FIXME make this test
  318 + def display_to?(user = nil)
  319 + return true if self.target == user
  320 + return false if !self.target.kind_of?(Environment) && self.target.person?
  321 +
  322 + if self.target.kind_of?(Environment)
  323 + user.is_admin?(self.target)
  324 + else
  325 + self.target.members.by_role(self.target.roles.reject {|r| !r.has_permission?('perform_task')}).include?(user)
  326 + end
  327 + end
  328 +
  329 +
272 330 protected
273 331  
274 332 # This method must be overrided in subclasses, and its implementation must do
... ...
app/models/user.rb
1 1 require 'digest/sha1'
2 2 require 'user_activation_job'
  3 +require 'securerandom'
3 4  
4 5 # User models the system users, and is generated by the acts_as_authenticated
5 6 # Rails generator.
... ... @@ -41,6 +42,14 @@ class User &lt; ActiveRecord::Base
41 42 alias_method_chain :human_attribute_name, :customization
42 43 end
43 44  
  45 + def self.build(user_data, person_data, environment)
  46 + user = User.new(user_data)
  47 + user.terms_of_use = environment.terms_of_use
  48 + user.environment = environment
  49 + user.person_data = person_data
  50 + user
  51 + end
  52 +
44 53 before_create do |user|
45 54 if user.environment.nil?
46 55 user.environment = Environment.default
... ... @@ -116,6 +125,7 @@ class User &lt; ActiveRecord::Base
116 125 validates_uniqueness_of :login, :email, :case_sensitive => false, :scope => :environment_id
117 126 before_save :encrypt_password
118 127 before_save :normalize_email, if: proc{ |u| u.email.present? }
  128 + before_save :generate_private_token_if_not_exist
119 129 validates_format_of :email, :with => Noosfero::Constants::EMAIL_FORMAT, :if => (lambda {|user| !user.email.blank?})
120 130  
121 131 validates_inclusion_of :terms_accepted, :in => [ '1' ], :if => lambda { |u| ! u.terms_of_use.blank? }, :message => N_('{fn} must be checked in order to signup.').fix_i18n
... ... @@ -131,13 +141,39 @@ class User &lt; ActiveRecord::Base
131 141  
132 142 u = self.has_login?(login, login, environment.id)
133 143 u = u.first if u.is_a?(ActiveRecord::Relation)
134   - u && u.authenticated?(password) ? u : nil
  144 +
  145 + if u && u.authenticated?(password)
  146 + u.generate_private_token_if_not_exist
  147 + return u
  148 + end
  149 + return nil
135 150 end
136 151  
137 152 def register_login
138 153 self.update_attribute :last_login_at, Time.now
139 154 end
140 155  
  156 + def generate_private_token
  157 + self.private_token = SecureRandom.hex
  158 + self.private_token_generated_at = DateTime.now
  159 + end
  160 +
  161 + def generate_private_token!
  162 + self.generate_private_token
  163 + save(:validate => false)
  164 + end
  165 +
  166 + def generate_private_token_if_not_exist
  167 + unless self.private_token
  168 + self.generate_private_token
  169 + end
  170 + end
  171 +
  172 + TOKEN_VALIDITY = 2.weeks
  173 + def private_token_expired?
  174 + self.private_token.nil? || (self.private_token_generated_at + TOKEN_VALIDITY < DateTime.now)
  175 + end
  176 +
141 177 # Activates the user in the database.
142 178 def activate
143 179 return false unless self.person
... ...
app/views/api/index.html.erb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +<h1>EndPoints</h1>
  2 +
  3 +<div style="float: right">
  4 +<%= s_('api-playground|Try the %s') % link_to('API Playground', '/api/playground') %>
  5 +</div>
  6 +
  7 +<%= endpoints.map do |endpoint|
  8 + app = endpoint.options[:app].to_s
  9 + unless app.blank?
  10 + content_tag(:h2, app.split('::').last.to_s, title: app) +
  11 + (content_tag :ul do
  12 + endpoint.routes.map do |route|
  13 + content_tag :li do
  14 + content_tag(:strong, route.route_method) + ' ' +
  15 + route.route_path.gsub(':version', content_tag(:b, route.route_version))
  16 + end
  17 + end.join "\n"
  18 + end)
  19 + end
  20 +end.join "\n" %>
... ...
app/views/api/playground.html.erb 0 → 100644
... ... @@ -0,0 +1,36 @@
  1 +<h1>API Playground</h1>
  2 +
  3 +<script>
  4 +var endpoints = <%=
  5 +endpoints.map do |endpoint|
  6 + app = endpoint.options[:app].to_s
  7 + unless app.blank?
  8 + endpoint.routes.map do |route|
  9 + {
  10 + method: route.route_method,
  11 + path: route.route_path.gsub(':version', route.route_version).split('(').first
  12 + }
  13 + end
  14 + end
  15 +end.flatten.compact.sort{|a,b|
  16 + a[:path]=='/api/v1/login' ? -1 :
  17 + b[:path]=='/api/v1/login' ? 1 :
  18 + a[:path] <=> b[:path]
  19 +}.to_json %>;
  20 +</script>
  21 +
  22 +<form id="api-form">
  23 + <label id="endpoint">Endpoint:
  24 + <select name="endpoint" onchange="playground.selEndpoint()"></select>
  25 + </label>
  26 + <label id="api-token">Token:
  27 + <%= tag :input, size: 32, placeholder: _('Use the login endpoint') %>
  28 + </label>
  29 +</form>
  30 +<div id="playground-ctrl">
  31 + <button onclick="playground.addFormParam()"><%=_('Add parameter')%></button>
  32 + <button onclick="playground.run()" style="font-weight:bold">&nbsp; <%=_('Run')%> &nbsp;</button>
  33 +</div>
  34 +<pre id="api-response" class="empty"></pre>
  35 +
  36 +<script src="/javascripts/api-playground.js"></script>
... ...
app/views/cms/edit.html.erb
... ... @@ -5,8 +5,6 @@
5 5  
6 6 <%= hidden_field_tag("type", @type) if @type %>
7 7  
8   - <%= hidden_field_tag('parent_id', @parent_id) if @parent_id %>
9   -
10 8 <%= hidden_field_tag('back_to', @back_to) %>
11 9  
12 10 <%= hidden_field_tag('success_back_to', @success_back_to) %>
... ...
app/views/content_viewer/_article_toolbar.html.erb
... ... @@ -29,10 +29,10 @@
29 29 <%= expirable_button @page, :locale, content, url %>
30 30 <% end %>
31 31  
32   - <%= modal_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)))) unless remove_content_button(:new, @page) %>
  32 + <%= modal_button(:new, label_for_new_article(@page), profile.admin_url.merge(:controller => 'cms', :action => 'new', :parent_id => (@page.folder? ? @page : @page.parent))) unless remove_content_button(:new, @page) %>
33 33  
34 34 <% content = content_tag('span', label_for_clone_article(@page)) %>
35   - <% url = profile.admin_url.merge({ :controller => 'cms', :action => 'new', :id => @page.id, :clone => true, :type => @page.class }) %>
  35 + <% url = profile.admin_url.merge({ :controller => 'cms', :action => 'new', :id => @page.id, :clone => true, :parent_id => (@page.folder? ? @page : @page.parent), :type => @page.class}) %>
36 36 <%= expirable_button @page, :clone, content, url %>
37 37 <% end %>
38 38  
... ...
app/views/profile/_profile_members_list.html.erb 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +<div id="profile-members-sort-options">
  2 + <%= label_tag("sort-#{role}", _("Sort by:")) %>
  3 + <%= select_tag("sort-#{role}",
  4 + options_for_select([
  5 + [_("Name A-Z"), 'asc'],
  6 + [_("Name Z-A"), 'desc'],
  7 + ], :selected => params[:sort])
  8 + ) %>
  9 +</div>
  10 +<ul class="profile-list-<%= role %>" >
  11 + <% users.each do |u| %>
  12 + <%= profile_image_link(u) %>
  13 + <% end %>
  14 +</ul>
  15 +
  16 +<%= pagination_links users, :param_name => 'npage' %>
... ...
app/views/profile/members.html.erb
1 1 <div class="common-profile-list-block">
2 2  
3   -<h1><%= _("%s's members") % profile.name %></h1>
  3 +<h1><%= _("Members (%d)") % @profile_members.total_entries %></h1>
  4 +<h2 class="community-name"><%= profile.name %></h2>
4 5  
5 6 <% cache_timeout(profile.members_cache_key(params), 4.hours) do %>
6   - <ul class='profile-list'>
7   - <% @members.each do |member| %>
8   - <%= profile_image_link(member) %>
9   - <% end %>
10   - </ul>
  7 + <div class="profile-members-tabs-container">
  8 + <% tabs = [] %>
  9 +
  10 + <% div_members = content_tag :div, :class => "profile-members" do
  11 + render :partial => 'profile_members_list',
  12 + :locals => {
  13 + :users => @profile_members,
  14 + :role => "members"
  15 + }
  16 + end %>
  17 +
  18 + <% tabs << {:title => _("%d Members") % @profile_members.total_entries,
  19 + :id => "members-tab",
  20 + :content => div_members
  21 + } %>
11 22  
12   - <div id='pagination-profiles'>
13   - <%= pagination_links @members, :param_name => 'npage' %>
14   - </div>
  23 + <% div_admins = content_tag :div, :class => "profile-admins" do
  24 + render :partial => 'profile_members_list',
  25 + :locals => {
  26 + :users => @profile_admins,
  27 + :role => "admins"
  28 + }
  29 + end %>
  30 +
  31 + <% tabs << {:title => _("%d Administrators") % @profile_admins.total_entries,
  32 + :id => "admins-tab",
  33 + :content => div_admins
  34 + } %>
  35 +
  36 + <%= render_tabs(tabs) %>
  37 + </div><!-- end of class="profile-members-tabs-container" -->
15 38 <% end %>
16 39  
17 40 <% button_bar do %>
... ... @@ -26,4 +49,7 @@
26 49 <% end %>
27 50 <% end %>
28 51  
29   -</div><!-- fim class="common-profile-list-block" -->
  52 +<%= hidden_field_tag "profile_url", @profile_members_url %>
  53 +</div><!-- end of class="common-profile-list-block" -->
  54 +
  55 +<%= javascript_include_tag "members_page.js" %>
... ...
app/views/profile_editor/_person.html.erb
... ... @@ -18,6 +18,14 @@
18 18  
19 19 <%= @plugins.dispatch(:profile_info_extra_contents).collect { |content| instance_exec(&content) }.join("") %>
20 20  
  21 + <div class="formfieldline">
  22 + <%= label_tag("private_token", _("Private Token")) %>
  23 + <div class="formfield type-text">
  24 + <%= text_field_tag("a", @profile.user.private_token, :size => 30) %>
  25 + </div>
  26 + </div>
  27 + <%= link_to("Reset token", {:controller => :profile_editor, :action => :reset_private_token, :id => @profile.id}, :class => "button with-text") %>
  28 +
21 29 <%= render :partial => 'person_form', :locals => {:f => f} %>
22 30  
23 31 <h2><%= _('Notification options') %></h2>
... ...
app/views/tasks/_suggest_article_accept_details.html.erb
... ... @@ -14,6 +14,5 @@
14 14 <%= labelled_form_field(_('Highlight this article'), a.check_box(:highlighted)) %>
15 15  
16 16 <%= a.hidden_field(:type) %>
17   -
18 17 <%= render :partial => 'shared/lead_and_body', :locals => {:tiny_mce => true, :f => a, :lead_id => task.id} %>
19 18 <% end %>
... ...
config.ru
1 1 # This file is used by Rack-based servers to start the application.
2 2  
3 3 require ::File.expand_path('../config/environment', __FILE__)
4   -run Noosfero::Application
  4 +
  5 +use Rack::Cors do
  6 + allow do
  7 + origins '*'
  8 + resource '/api/*', :headers => :any, :methods => [:get, :post]
  9 + end
  10 +end
  11 +
  12 +rails_app = Rack::Builder.new do
  13 + if ENV['RAILS_RELATIVE_URL_ROOT']
  14 + map ENV['RAILS_RELATIVE_URL_ROOT'] do
  15 + run Noosfero::Application
  16 + end
  17 + else
  18 + run Noosfero::Application
  19 + end
  20 +end
  21 +
  22 +run Rack::Cascade.new([
  23 + Noosfero::API::API,
  24 + rails_app
  25 +])
... ...
config/initializers/session.rb
1 1 ActionDispatch::Reloader.to_prepare do
  2 + require_relative '../../app/models/session'
2 3 ActiveRecord::SessionStore.session_class = Session
3 4 end
4 5  
... ...
config/noosfero.yml.dist
... ... @@ -11,7 +11,14 @@ development:
11 11 max_upload_size: 5MB
12 12 hours_until_user_activation_check: 72
13 13 exclude_profile_identifier_pattern: index(\..*)?|home(\..*)?
  14 + api_recaptcha_site_key: '6LdsWAcTAAAAAChTUUD6yu9fCDhdIZzNd7F53zf-'
  15 + api_recaptcha_private_key: '6LdsWAcTAAAAAB6maB_HalVyCc4asDAxPxloIMvY'
  16 + api_recaptcha_verify_uri: 'https://www.google.com/recaptcha/api/siteverify'
14 17  
15 18 test:
16 19  
17 20 production:
  21 + api_recaptcha_site_key: '6LcLPAcTAAAAAKsd0bxY_TArhD_A7OL19SRCW7_i'
  22 + api_recaptcha_private_key: '6LcLPAcTAAAAAE36SN1M2w1I7Hn8upwXYZ_YQZ5-'
  23 + api_recaptcha_verify_uri: 'https://www.google.com/recaptcha/api/siteverify'
  24 +
18 25 \ No newline at end of file
... ...
config/routes.rb
... ... @@ -22,6 +22,7 @@ Noosfero::Application.routes.draw do
22 22 root :to => 'home#index', :constraints => EnvironmentDomainConstraint.new
23 23  
24 24 match 'site(/:action)', :controller => 'home'
  25 + match 'api(/:action)', :controller => 'api'
25 26  
26 27 match 'images(/*stuff)' => 'not_found#nothing'
27 28 match 'stylesheets(/*stuff)' => 'not_found#nothing'
... ...
config/thin.yml.dist
... ... @@ -1,13 +0,0 @@
1   ----
2   -pid: tmp/pids/thin.pid
3   -address: 127.0.0.1
4   -user: noosfero
5   -timeout: 30
6   -port: 50000
7   -log: log/thin.log
8   -max_conns: 1024
9   -servers: 1
10   -max_persistent_conns: 512
11   -environment: production
12   -daemonize: true
13   -chdir: /usr/share/noosfero
config/unicorn.rb.dist 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +listen "127.0.0.1:3000"
  2 +
  3 +worker_processes 1
  4 +
  5 +# vim: ft=ruby
... ...
db/migrate/20150223180807_add_private_token_info_to_users.rb 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +class AddPrivateTokenInfoToUsers < ActiveRecord::Migration
  2 + def self.up
  3 + add_column :users, :private_token, :string
  4 + add_column :users, :private_token_generated_at, :datetime
  5 + end
  6 +
  7 + def self.down
  8 + remove_column :users, :private_token
  9 + remove_column :users, :private_token_generated_at
  10 + end
  11 +end
... ...
db/schema.rb
... ... @@ -754,24 +754,26 @@ ActiveRecord::Schema.define(:version =&gt; 20150722042714) do
754 754 create_table "users", :force => true do |t|
755 755 t.string "login"
756 756 t.string "email"
757   - t.string "crypted_password", :limit => 40
758   - t.string "salt", :limit => 40
  757 + t.string "crypted_password", :limit => 40
  758 + t.string "salt", :limit => 40
759 759 t.datetime "created_at"
760 760 t.datetime "updated_at"
761 761 t.string "remember_token"
762 762 t.datetime "remember_token_expires_at"
763 763 t.text "terms_of_use"
764   - t.string "terms_accepted", :limit => 1
  764 + t.string "terms_accepted", :limit => 1
765 765 t.integer "environment_id"
766 766 t.string "password_type"
767   - t.boolean "enable_email", :default => false
768   - t.string "last_chat_status", :default => ""
769   - t.string "chat_status", :default => ""
  767 + t.boolean "enable_email", :default => false
  768 + t.string "last_chat_status", :default => ""
  769 + t.string "chat_status", :default => ""
770 770 t.datetime "chat_status_at"
771   - t.string "activation_code", :limit => 40
  771 + t.string "activation_code", :limit => 40
772 772 t.datetime "activated_at"
773 773 t.string "return_to"
774 774 t.datetime "last_login_at"
  775 + t.string "private_token"
  776 + t.datetime "private_token_generated_at"
775 777 end
776 778  
777 779 create_table "validation_infos", :force => true do |t|
... ...
debian/apache2/conf.d/noosfero-cluster.conf
... ... @@ -1,8 +0,0 @@
1   -<Proxy balancer://noosfero>
2   - Include /etc/noosfero/apache/cluster.conf
3   - Order Allow,Deny
4   - Allow from All
5   -</Proxy>
6   -
7   -# vim: ft=apache
8   -
debian/apache2/virtualhost.conf
... ... @@ -15,7 +15,11 @@ RewriteRule ^/$ /index.html [QSA]
15 15 RewriteRule ^([^.]+)$ $1.html [QSA]
16 16  
17 17 RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
18   -RewriteRule ^.*$ balancer://noosfero%{REQUEST_URI} [P,QSA,L]
  18 +RewriteRule ^.*$ http://127.0.0.1:50000%{REQUEST_URI} [P,QSA,L]
  19 +<Proxy http://127.0.0.1:50000>
  20 + Order Allow,Deny
  21 + Allow from All
  22 +</Proxy>
19 23  
20 24 ErrorDocument 503 /503.html
21 25  
... ...
debian/changelog
  1 +noosfero (1.3~0.0) UNRELEASED; urgency=medium
  2 +
  3 + * Noosfero 1.3 ALPHA 0
  4 +
  5 + -- Antonio Terceiro <terceiro@debian.org> Wed, 09 Sep 2015 21:27:05 -0300
  6 +
1 7 noosfero (1.2) wheezy; urgency=low
2 8  
3 9 * Noosfero 1.2
... ...
debian/control
... ... @@ -49,12 +49,17 @@ Depends: adduser,
49 49 ruby-feedparser,
50 50 ruby-feedparser (>= 0.7-3~),
51 51 ruby-gettext,
  52 + ruby-grape,
  53 + ruby-grape-entity,
  54 + ruby-grape-logging,
52 55 ruby-memcache-client,
53 56 ruby-minitest,
54 57 ruby-nokogiri,
55 58 ruby-pg,
56 59 ruby-progressbar,
57 60 ruby-rack (>= 1.4.5-2~),
  61 + ruby-rack-contrib,
  62 + ruby-rack-cors,
58 63 ruby-rails-autolink,
59 64 ruby-redcloth,
60 65 ruby-rest-client,
... ... @@ -65,7 +70,7 @@ Depends: adduser,
65 70 ruby-whenever,
66 71 ruby-will-paginate (>= 2.3.12-1~),
67 72 tango-icon-theme,
68   - thin,
  73 + unicorn (>= 4.8),
69 74 ${misc:Depends}
70 75 Recommends: postgresql, postgresql-client
71 76 Description: free web-based platform for social networks
... ...
debian/noosfero-apache.install
1   -debian/apache2/conf.d/noosfero-cluster.conf etc/apache2/conf.d
2 1 debian/apache2/virtualhost.conf etc/noosfero/apache
3 2 debian/update-noosfero-apache usr/sbin
... ...
debian/noosfero.install
... ... @@ -19,7 +19,7 @@ debian/noosfero-check-dbconfig usr/sbin
19 19 debian/noosfero-console usr/sbin
20 20 debian/noosfero-runner usr/sbin
21 21 debian/noosfero.yml etc/noosfero
22   -debian/thin.yml etc/noosfero
  22 +debian/unicorn.rb etc/noosfero
23 23 doc usr/share/noosfero
24 24 doc/noosfero usr/share/noosfero/doc
25 25 etc/init.d/noosfero etc/init.d
... ...
debian/noosfero.links
1 1 var/tmp/noosfero usr/share/noosfero/tmp
2 2 var/log/noosfero usr/share/noosfero/log
3 3 etc/noosfero/database.yml usr/share/noosfero/config/database.yml
4   -etc/noosfero/thin.yml usr/share/noosfero/config/thin.yml
  4 +etc/noosfero/unicorn.rb usr/share/noosfero/config/unicorn.rb
5 5 etc/noosfero/plugins usr/share/noosfero/config/plugins
6 6 etc/noosfero/noosfero.yml usr/share/noosfero/config/noosfero.yml
7 7 etc/noosfero/local.rb usr/share/noosfero/config/local.rb
... ...
debian/noosfero.postinst
... ... @@ -10,23 +10,23 @@ makedir() {
10 10 }
11 11  
12 12  
13   -# migrate mongrel configuration to thin
14   -mongrel_config=/etc/noosfero/mongrel_cluster.yml
  13 +# migrate thin configuration to unicorn
  14 +unicorn_config=/etc/noosfero/unicorn.rb
15 15 thin_config=/etc/noosfero/thin.yml
16   -mongrel_orig_sha1=602c642c03f79349969c08330112a6f29d4c32aa
17   -if [ -r $mongrel_config ]; then
18   - mongrel_sha1=$(sha1sum $mongrel_config | awk '{print $1}')
19   - if [ "$mongrel_sha1" != "$mongrel_orig_sha1" ]; then
20   - port=$(awk '{ if ($1 == "port:") { print($2) } }' $mongrel_config)
21   - servers=$(awk '{ if ($1 == "servers:") { print($2) } }' $mongrel_config)
  16 +thin_orig_sha1=47cee6728a7896a13f4d66544086ab88b02e89a7
  17 +if [ -r $thin_config ]; then
  18 + thin_sha1=$(sha1sum $thin_config | awk '{print $1}')
  19 + if [ "$thin_sha1" != "$thin_orig_sha1" ]; then
  20 + port=$(awk '{ if ($1 == "port:") { print($2) } }' $thin_config)
  21 + servers=$(awk '{ if ($1 == "servers:") { print($2) } }' $thin_config)
22 22 if test -n "$port" && test "$port" -ne 50000 || test -n "$servers" && test "$servers" -ne 1 ; then
23   - # mongrel configuration was changed; update thin configuration
  23 + # thin configuration was changed; update unicorn configuration
24 24 # accordingly
25   - sed -i -e "s/port: .*/port: $port/" $thin_config
26   - sed -i -e "s/servers: .*/servers: $servers/" $thin_config
  25 + sed -i -e "s/listen.*/listen '127.0.0.1:$port'/" $unicorn_config
  26 + sed -i -e "s/worker_processes.*/worker_processes $servers/" $unicorn_config
27 27 fi
28 28 fi
29   - mv $mongrel_config $mongrel_config.bak
  29 + mv $thin_config $thin_config.bak
30 30 fi
31 31  
32 32 # create user noosfero in a portable way, while creating the log directory.
... ...
debian/thin.yml
... ... @@ -1,13 +0,0 @@
1   ----
2   -pid: tmp/pids/thin.pid
3   -address: 127.0.0.1
4   -user: noosfero
5   -timeout: 0
6   -port: 50000
7   -log: log/thin.log
8   -max_conns: 1024
9   -servers: 1
10   -max_persistent_conns: 512
11   -environment: production
12   -daemonize: true
13   -chdir: /usr/share/noosfero
debian/unicorn.rb 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +listen "127.0.0.1:50000"
  2 +
  3 +worker_processes `nproc`.to_i
... ...
debian/update-noosfero-apache
... ... @@ -2,20 +2,9 @@
2 2  
3 3 set -e
4 4  
5   -# automatically update configuration, but if package noosfero is also installed
  5 +# automatically update configuration, but only if package noosfero is also
  6 +# installed
6 7 if test -x /usr/share/noosfero/script/apacheconf; then
7   - datadir="/etc/noosfero/apache"
8   - mongrel_file="$datadir/mongrel.conf"
9   - cluster_file="$datadir/cluster.conf"
10   - if test -e "$cluster_file"; then
11   - echo "Overwriting $cluster_file ..."
12   - fi
13   - /usr/share/noosfero/script/apacheconf thin > "$cluster_file"
14   - if test -e "$mongrel_file"; then
15   - echo "Moving existing $mongrel_file away ..."
16   - mv "$mongrel_file" "$mongrel_file".bak
17   - (cd $datadir && ln -sf cluster.conf mongrel.conf)
18   - fi
19 8  
20 9 apache_site='/etc/apache2/sites-available/noosfero'
21 10 if ! test -e "$apache_site"; then
... ...
gitignore.example
... ... @@ -10,7 +10,7 @@ config/session.secret
10 10 config/noosfero.yml
11 11 config/mongrel_cluster.yml
12 12 config/plugins
13   -config/thin.yml
  13 +config/unicorn.rb
14 14 config/local.rb
15 15 index
16 16 locale
... ...
lib/authenticated_system.rb
... ... @@ -2,17 +2,21 @@ module AuthenticatedSystem
2 2  
3 3 protected
4 4  
5   - # See impl. from http://stackoverflow.com/a/2513456/670229
6   - def self.included? base
7   - base.around_filter do
  5 + def self.included base
  6 + # See impl. from http://stackoverflow.com/a/2513456/670229
  7 + base.around_filter do |&block|
8 8 begin
9 9 User.current = current_user
10   - yield
  10 + block.call
11 11 ensure
12 12 # to address the thread variable leak issues in Puma/Thin webserver
13 13 User.current = nil
14 14 end
15 15 end
  16 +
  17 + # Inclusion hook to make #current_user and #logged_in?
  18 + # available as ActionView helper methods.
  19 + base.send :helper_method, :current_user, :logged_in?
16 20 end
17 21  
18 22 # Returns true or false if the user is logged in.
... ... @@ -134,12 +138,6 @@ module AuthenticatedSystem
134 138 end
135 139 end
136 140  
137   - # Inclusion hook to make #current_user and #logged_in?
138   - # available as ActionView helper methods.
139   - def self.included(base)
140   - base.send :helper_method, :current_user, :logged_in?
141   - end
142   -
143 141 # When called with before_filter :login_from_cookie will check for an :auth_token
144 142 # cookie and log the user back in if apropriate
145 143 def login_from_cookie
... ...
lib/noosfero/api/api.rb 0 → 100644
... ... @@ -0,0 +1,82 @@
  1 +require 'grape'
  2 +#require 'rack/contrib'
  3 +
  4 +Dir["#{Rails.root}/lib/noosfero/api/*.rb"].each {|file| require file unless file =~ /api\.rb/}
  5 +module Noosfero
  6 + module API
  7 + class API < Grape::API
  8 + use Rack::JSONP
  9 +
  10 + logger = Logger.new(File.join(Rails.root, 'log', "#{ENV['RAILS_ENV'] || 'production'}_api.log"))
  11 + logger.formatter = GrapeLogging::Formatters::Default.new
  12 + #use GrapeLogging::Middleware::RequestLogger, { logger: logger }
  13 +
  14 + rescue_from :all do |e|
  15 + logger.error e
  16 + end
  17 +
  18 + @@NOOSFERO_CONF = nil
  19 +
  20 + def self.NOOSFERO_CONF
  21 + if @@NOOSFERO_CONF
  22 + @@NOOSFERO_CONF
  23 + else
  24 + file = Rails.root.join('config', 'noosfero.yml')
  25 + @@NOOSFERO_CONF = File.exists?(file) ? YAML.load_file(file)[Rails.env] || {} : {}
  26 + end
  27 + end
  28 +
  29 + before { setup_multitenancy }
  30 + before { detect_stuff_by_domain }
  31 + before { filter_disabled_plugins_endpoints }
  32 + after { set_session_cookie }
  33 +
  34 + version 'v1'
  35 + prefix "api"
  36 + format :json
  37 + content_type :txt, "text/plain"
  38 +
  39 + helpers APIHelpers
  40 +
  41 + mount V1::Articles
  42 + mount V1::Comments
  43 + mount V1::Communities
  44 + mount V1::People
  45 + mount V1::Enterprises
  46 + mount V1::Categories
  47 + mount V1::Tasks
  48 + mount Session
  49 +
  50 + # hook point which allow plugins to add Grape::API extensions to API::API
  51 + #finds for plugins which has api mount points classes defined (the class should extends Grape::API)
  52 + @plugins = Noosfero::Plugin.all.map { |p| p.constantize }
  53 + @plugins.each do |klass|
  54 + if klass.public_methods.include? :api_mount_points
  55 + klass.api_mount_points.each do |mount_class|
  56 + mount mount_class if mount_class && ( mount_class < Grape::API )
  57 + end
  58 + end
  59 + end
  60 +
  61 + def self.endpoint_unavailable?(endpoint, environment)
  62 + api_class = endpoint.options[:app] || endpoint.options[:for]
  63 + if api_class.present?
  64 + klass = api_class.name.deconstantize.constantize
  65 + return klass < Noosfero::Plugin && !environment.plugin_enabled?(klass)
  66 + end
  67 + end
  68 +
  69 + class << self
  70 + def endpoints_with_plugins(environment = nil)
  71 + if environment.present?
  72 + cloned_endpoints = endpoints_without_plugins.dup
  73 + cloned_endpoints.delete_if { |endpoint| endpoint_unavailable?(endpoint, environment) }
  74 + else
  75 + endpoints_without_plugins
  76 + end
  77 + end
  78 + alias_method_chain :endpoints, :plugins
  79 + end
  80 + end
  81 + end
  82 +end
... ...
lib/noosfero/api/entities.rb 0 → 100644
... ... @@ -0,0 +1,121 @@
  1 +module Noosfero
  2 + module API
  3 + module Entities
  4 +
  5 + Entity.format_with :timestamp do |date|
  6 + date.strftime('%Y/%m/%d %H:%M:%S') if date
  7 + end
  8 +
  9 + class Image < Entity
  10 + root 'images', 'image'
  11 +
  12 + expose :url do |image, options|
  13 + image.public_filename
  14 + end
  15 +
  16 + expose :icon_url do |image, options|
  17 + image.public_filename(:icon)
  18 + end
  19 +
  20 + expose :minor_url do |image, options|
  21 + image.public_filename(:minor)
  22 + end
  23 +
  24 + expose :portrait_url do |image, options|
  25 + image.public_filename(:portrait)
  26 + end
  27 +
  28 + expose :thumb_url do |image, options|
  29 + image.public_filename(:thumb)
  30 + end
  31 + end
  32 +
  33 + class Profile < Entity
  34 + expose :identifier, :name, :id
  35 + expose :created_at, :format_with => :timestamp
  36 + expose :updated_at, :format_with => :timestamp
  37 + expose :image, :using => Image
  38 + end
  39 +
  40 + class User < Entity
  41 + expose :id
  42 + expose :login
  43 + end
  44 +
  45 + class Person < Profile
  46 + root 'people', 'person'
  47 + expose :user, :using => User
  48 + end
  49 + class Enterprise < Profile
  50 + root 'enterprises', 'enterprise'
  51 + end
  52 + class Community < Profile
  53 + root 'communities', 'community'
  54 + expose :description
  55 + expose :categories
  56 + expose :members, :using => Person
  57 + end
  58 +
  59 + class CategoryBase < Entity
  60 + root 'categories', 'category'
  61 + expose :name, :id
  62 + end
  63 +
  64 + class Category < CategoryBase
  65 + root 'categories', 'category'
  66 + expose :slug
  67 + expose :full_name do |category, options|
  68 + category.full_name
  69 + end
  70 + expose :parent, :using => CategoryBase, if: { parent: true }
  71 + expose :children, :using => CategoryBase, if: { children: true }
  72 + expose :image, :using => Image
  73 + end
  74 +
  75 + class ArticleBase < Entity
  76 + root 'articles', 'article'
  77 + expose :id
  78 + expose :body
  79 + expose :abstract
  80 + expose :created_at, :format_with => :timestamp
  81 + expose :updated_at, :format_with => :timestamp
  82 + expose :title, :documentation => {:type => "String", :desc => "Title of the article"}
  83 + expose :created_by, :as => :author, :using => Profile
  84 + expose :profile, :using => Profile
  85 + expose :categories, :using => Category
  86 + expose :image, :using => Image
  87 + #TODO Apply vote stuff in core and make this test
  88 + expose :votes_for
  89 + expose :votes_against
  90 + expose :setting
  91 + expose :position
  92 + expose :hits
  93 + expose :path
  94 + end
  95 +
  96 + class Article < ArticleBase
  97 + root 'articles', 'article'
  98 + expose :parent, :using => ArticleBase
  99 + expose :children, :using => ArticleBase
  100 + end
  101 +
  102 + class Comment < Entity
  103 + root 'comments', 'comment'
  104 + expose :body, :title, :id
  105 + expose :created_at, :format_with => :timestamp
  106 + expose :author, :using => Profile
  107 + end
  108 +
  109 + class UserLogin < User
  110 + expose :private_token
  111 + end
  112 +
  113 + class Task < Entity
  114 + root 'tasks', 'task'
  115 + expose :id
  116 + expose :type
  117 + end
  118 +
  119 + end
  120 + end
  121 +end
... ...
lib/noosfero/api/entity.rb 0 → 100644
... ... @@ -0,0 +1,39 @@
  1 +class Noosfero::API::Entity < Grape::Entity
  2 +
  3 + def initialize(object, options = {})
  4 + object = nil if object.is_a? Exception
  5 + super object, options
  6 + end
  7 +
  8 + def self.represent(objects, options = {})
  9 + if options[:has_exception]
  10 + data = super objects, options.merge(is_inner_data: true)
  11 + if objects.is_a? Exception
  12 + data.merge ok: false, error: {
  13 + type: objects.class.name,
  14 + message: objects.message
  15 + }
  16 + else
  17 + data = data.serializable_hash if data.is_a? Noosfero::API::Entity
  18 + data.merge ok: true, error: { type: 'Success', message: '' }
  19 + end
  20 + else
  21 + super objects, options
  22 + end
  23 + end
  24 +
  25 + def self.fields_condition(fields)
  26 + lambda do |object, options|
  27 + return true if options[:fields].blank?
  28 + fields.map { |field| options[:fields].include?(field.to_s)}.grep(true).present?
  29 + end
  30 + end
  31 +
  32 + def self.expose(*args, &block)
  33 + hash = args.last.is_a?(Hash) ? args.pop : {}
  34 + hash.merge!({:if => fields_condition(args)}) if hash[:if].blank?
  35 + args << hash
  36 + super
  37 + end
  38 +
  39 +end
... ...
lib/noosfero/api/helpers.rb 0 → 100644
... ... @@ -0,0 +1,314 @@
  1 +module Noosfero
  2 + module API
  3 + module APIHelpers
  4 + PRIVATE_TOKEN_PARAM = :private_token
  5 + DEFAULT_ALLOWED_PARAMETERS = [:parent_id, :from, :until, :content_type]
  6 +
  7 + def current_user
  8 + private_token = (params[PRIVATE_TOKEN_PARAM] || headers['Private-Token']).to_s
  9 + @current_user ||= User.find_by_private_token(private_token)
  10 + @current_user = nil if !@current_user.nil? && @current_user.private_token_expired?
  11 + @current_user
  12 + end
  13 +
  14 + def current_person
  15 + current_user.person unless current_user.nil?
  16 + end
  17 +
  18 + def logout
  19 + @current_user = nil
  20 + end
  21 +
  22 + def environment
  23 + @environment
  24 + end
  25 +
  26 + def limit
  27 + limit = params[:limit].to_i
  28 + limit = default_limit if limit <= 0
  29 + limit
  30 + end
  31 +
  32 + def period(from_date, until_date)
  33 + return nil if from_date.nil? && until_date.nil?
  34 +
  35 + begin_period = from_date.nil? ? Time.at(0).to_datetime : from_date
  36 + end_period = until_date.nil? ? DateTime.now : until_date
  37 +
  38 + begin_period..end_period
  39 + end
  40 +
  41 + def parse_content_type(content_type)
  42 + return nil if content_type.blank?
  43 + content_type.split(',').map do |content_type|
  44 + content_type.camelcase
  45 + end
  46 + end
  47 +
  48 + ARTICLE_TYPES = ['Article'] + Article.descendants.map{|a| a.to_s}
  49 + TASK_TYPES = ['Task'] + Task.descendants.map{|a| a.to_s}
  50 +
  51 + def find_article(articles, id)
  52 + article = articles.find(id)
  53 + article.display_to?(current_user.person) ? article : forbidden!
  54 + end
  55 +
  56 + def post_article(asset, params)
  57 + return forbidden! unless current_person.can_post_content?(asset)
  58 +
  59 + klass_type= params[:content_type].nil? ? 'TinyMceArticle' : params[:content_type]
  60 + return forbidden! unless ARTICLE_TYPES.include?(klass_type)
  61 +
  62 + article = klass_type.constantize.new(params[:article])
  63 + article.last_changed_by = current_person
  64 + article.created_by= current_person
  65 + article.profile = asset
  66 +
  67 + if !article.save
  68 + render_api_errors!(article.errors.full_messages)
  69 + end
  70 + present article, :with => Entities::Article, :fields => params[:fields]
  71 + end
  72 +
  73 + def present_article(asset)
  74 + article = find_article(asset.articles, params[:id])
  75 + present article, :with => Entities::Article, :fields => params[:fields]
  76 + end
  77 +
  78 + def present_articles(asset)
  79 + articles = select_filtered_collection_of(asset, 'articles', params)
  80 + articles = articles.display_filter(current_person, nil)
  81 + present articles, :with => Entities::Article, :fields => params[:fields]
  82 + end
  83 +
  84 + def find_task(asset, id)
  85 + task = asset.tasks.find(id)
  86 + current_person.has_permission?(task.permission, asset) ? task : forbidden!
  87 + end
  88 +
  89 + def post_task(asset, params)
  90 + klass_type= params[:content_type].nil? ? 'Task' : params[:content_type]
  91 + return forbidden! unless TASK_TYPES.include?(klass_type)
  92 +
  93 + task = klass_type.constantize.new(params[:task])
  94 + task.requestor_id = current_person.id
  95 + task.target_id = asset.id
  96 + task.target_type = 'Profile'
  97 +
  98 + if !task.save
  99 + render_api_errors!(task.errors.full_messages)
  100 + end
  101 + present task, :with => Entities::Task, :fields => params[:fields]
  102 + end
  103 +
  104 + def present_task(asset)
  105 + task = find_task(asset, params[:id])
  106 + present task, :with => Entities::Task, :fields => params[:fields]
  107 + end
  108 +
  109 + def present_tasks(asset)
  110 + tasks = select_filtered_collection_of(asset, 'tasks', params)
  111 + tasks = tasks.select {|t| current_person.has_permission?(t.permission, asset)}
  112 + return forbidden! if tasks.empty? && !current_person.has_permission?(:perform_task, asset)
  113 + present tasks, :with => Entities::Task, :fields => params[:fields]
  114 + end
  115 +
  116 + def make_conditions_with_parameter(params = {})
  117 + parsed_params = parser_params(params)
  118 + conditions = {}
  119 + from_date = DateTime.parse(parsed_params.delete(:from)) if parsed_params[:from]
  120 + until_date = DateTime.parse(parsed_params.delete(:until)) if parsed_params[:until]
  121 +
  122 + conditions[:type] = parse_content_type(parsed_params.delete(:content_type)) unless parsed_params[:content_type].nil?
  123 +
  124 + conditions[:created_at] = period(from_date, until_date) if from_date || until_date
  125 + conditions.merge!(parsed_params)
  126 +
  127 + conditions
  128 + end
  129 +
  130 + def make_order_with_parameters(params)
  131 + params[:order] || "created_at DESC"
  132 + end
  133 +
  134 + def make_page_number_with_parameters(params)
  135 + params[:page] || 1
  136 + end
  137 +
  138 + def make_per_page_with_parameters(params)
  139 + params[:per_page] ||= limit
  140 + params[:per_page].to_i
  141 + end
  142 +
  143 + def make_timestamp_with_parameters_and_method(params, method)
  144 + timestamp = nil
  145 + if params[:timestamp]
  146 + datetime = DateTime.parse(params[:timestamp])
  147 + table_name = method.to_s.singularize.camelize.constantize.table_name
  148 + timestamp = "#{table_name}.updated_at >= '#{datetime}'"
  149 + end
  150 +
  151 + timestamp
  152 + end
  153 +
  154 + def by_reference(scope, params)
  155 + if params[:reference_id]
  156 + created_at = scope.find(params[:reference_id]).created_at
  157 + scope.send("#{params.key?(:oldest) ? 'older_than' : 'younger_than'}", created_at)
  158 + else
  159 + scope
  160 + end
  161 + end
  162 +
  163 + def select_filtered_collection_of(object, method, params)
  164 + conditions = make_conditions_with_parameter(params)
  165 + order = make_order_with_parameters(params)
  166 + page_number = make_page_number_with_parameters(params)
  167 + per_page = make_per_page_with_parameters(params)
  168 + timestamp = make_timestamp_with_parameters_and_method(params, method)
  169 +
  170 + objects = object.send(method)
  171 + objects = by_reference(objects, params)
  172 +
  173 + objects = objects.where(conditions).where(timestamp).page(page_number).per_page(per_page).order(order)
  174 +
  175 + objects
  176 + end
  177 +
  178 + def authenticate!
  179 + unauthorized! unless current_user
  180 + end
  181 +
  182 + # Checks the occurrences of uniqueness of attributes, each attribute must be present in the params hash
  183 + # or a Bad Request error is invoked.
  184 + #
  185 + # Parameters:
  186 + # keys (unique) - A hash consisting of keys that must be unique
  187 + def unique_attributes!(obj, keys)
  188 + keys.each do |key|
  189 + cant_be_saved_request!(key) if obj.send("find_by_#{key.to_s}", params[key])
  190 + end
  191 + end
  192 +
  193 + def attributes_for_keys(keys)
  194 + attrs = {}
  195 + keys.each do |key|
  196 + attrs[key] = params[key] if params[key].present? or (params.has_key?(key) and params[key] == false)
  197 + end
  198 + attrs
  199 + end
  200 +
  201 + def verify_recaptcha_v2(remote_ip, g_recaptcha_response, private_key, api_recaptcha_verify_uri)
  202 + verify_hash = {
  203 + "secret" => private_key,
  204 + "remoteip" => remote_ip,
  205 + "response" => g_recaptcha_response
  206 + }
  207 + uri = URI(api_recaptcha_verify_uri)
  208 + https = Net::HTTP.new(uri.host, uri.port)
  209 + https.use_ssl = true
  210 + request = Net::HTTP::Post.new(uri.path)
  211 + request.set_form_data(verify_hash)
  212 + JSON.parse(https.request(request).body)
  213 + end
  214 +
  215 + ##########################################
  216 + # error helpers #
  217 + ##########################################
  218 +
  219 + def not_found!
  220 + render_api_error!('404 Not found', 404)
  221 + end
  222 +
  223 + def forbidden!
  224 + render_api_error!('403 Forbidden', 403)
  225 + end
  226 +
  227 + def cant_be_saved_request!(attribute)
  228 + message = _("(Invalid request) #{attribute} can't be saved")
  229 + render_api_error!(message, 400)
  230 + end
  231 +
  232 + def bad_request!(attribute)
  233 + message = _("(Bad request) #{attribute} not given")
  234 + render_api_error!(message, 400)
  235 + end
  236 +
  237 + def something_wrong!
  238 + message = _("Something wrong happened")
  239 + render_api_error!(message, 400)
  240 + end
  241 +
  242 + def unauthorized!
  243 + render_api_error!(_('Unauthorized'), 401)
  244 + end
  245 +
  246 + def not_allowed!
  247 + render_api_error!(_('Method Not Allowed'), 405)
  248 + end
  249 +
  250 + def render_api_error!(message, status)
  251 + error!({'message' => message, :code => status}, status)
  252 + end
  253 +
  254 + def render_api_errors!(messages)
  255 + render_api_error!(messages.join(','), 400)
  256 + end
  257 + protected
  258 +
  259 + def set_session_cookie
  260 + cookies['_noosfero_api_session'] = { value: @current_user.private_token, httponly: true } if @current_user.present?
  261 + end
  262 +
  263 + def setup_multitenancy
  264 + Noosfero::MultiTenancy.setup!(request.host)
  265 + end
  266 +
  267 + def detect_stuff_by_domain
  268 + @domain = Domain.find_by_name(request.host)
  269 + if @domain.nil?
  270 + @environment = Environment.default
  271 + if @environment.nil? && Rails.env.development?
  272 + # This should only happen in development ...
  273 + @environment = Environment.create!(:name => "Noosfero", :is_default => true)
  274 + end
  275 + else
  276 + @environment = @domain.environment
  277 + end
  278 + end
  279 +
  280 + def filter_disabled_plugins_endpoints
  281 + not_found! if Noosfero::API::API.endpoint_unavailable?(self, !@environment)
  282 + end
  283 +
  284 + private
  285 +
  286 + def parser_params(params)
  287 + parsed_params = {}
  288 + params.map do |k,v|
  289 + parsed_params[k.to_sym] = v if DEFAULT_ALLOWED_PARAMETERS.include?(k.to_sym)
  290 + end
  291 + parsed_params
  292 + end
  293 +
  294 + def default_limit
  295 + 20
  296 + end
  297 +
  298 + def parse_content_type(content_type)
  299 + return nil if content_type.blank?
  300 + content_type.split(',').map do |content_type|
  301 + content_type.camelcase
  302 + end
  303 + end
  304 +
  305 + def period(from_date, until_date)
  306 + begin_period = from_date.nil? ? Time.at(0).to_datetime : from_date
  307 + end_period = until_date.nil? ? DateTime.now : until_date
  308 +
  309 + begin_period..end_period
  310 + end
  311 +
  312 + end
  313 + end
  314 +end
... ...
lib/noosfero/api/session.rb 0 → 100644
... ... @@ -0,0 +1,61 @@
  1 +require "uri"
  2 +
  3 +module Noosfero
  4 + module API
  5 +
  6 + class Session < Grape::API
  7 +
  8 + # Login to get token
  9 + #
  10 + # Parameters:
  11 + # login (*required) - user login or email
  12 + # password (required) - user password
  13 + #
  14 + # Example Request:
  15 + # POST http://localhost:3000/api/v1/login?login=adminuser&password=admin
  16 + post "/login" do
  17 + user ||= User.authenticate(params[:login], params[:password], environment)
  18 +
  19 + return unauthorized! unless user
  20 + @current_user = user
  21 + present user, :with => Entities::UserLogin
  22 + end
  23 +
  24 + # Create user.
  25 + #
  26 + # Parameters:
  27 + # email (required) - Email
  28 + # password (required) - Password
  29 + # login - login
  30 + # Example Request:
  31 + # POST /register?email=some@mail.com&password=pas&login=some
  32 + params do
  33 + requires :email, type: String, desc: _("Email")
  34 + requires :login, type: String, desc: _("Login")
  35 + requires :password, type: String, desc: _("Password")
  36 + end
  37 + post "/register" do
  38 + unique_attributes! User, [:email, :login]
  39 + attrs = attributes_for_keys [:email, :login, :password]
  40 + attrs[:password_confirmation] = attrs[:password]
  41 +
  42 + #Commented for stress tests
  43 +
  44 + # remote_ip = (request.respond_to?(:remote_ip) && request.remote_ip) || (env && env['REMOTE_ADDR'])
  45 + # private_key = API.NOOSFERO_CONF['api_recaptcha_private_key']
  46 + # api_recaptcha_verify_uri = API.NOOSFERO_CONF['api_recaptcha_verify_uri']
  47 + # captcha_result = verify_recaptcha_v2(remote_ip, params['g-recaptcha-response'], private_key, api_recaptcha_verify_uri)
  48 + user = User.new(attrs)
  49 +# if captcha_result["success"] and user.save
  50 + if user.save
  51 + user.activate
  52 + user.generate_private_token!
  53 + present user, :with => Entities::UserLogin
  54 + else
  55 + message = user.errors.to_json
  56 + render_api_error!(message, 400)
  57 + end
  58 + end
  59 + end
  60 + end
  61 +end
... ...
lib/noosfero/api/v1/articles.rb 0 → 100644
... ... @@ -0,0 +1,121 @@
  1 +module Noosfero
  2 + module API
  3 + module V1
  4 + class Articles < Grape::API
  5 + before { authenticate! }
  6 +
  7 + ARTICLE_TYPES = Article.descendants.map{|a| a.to_s}
  8 +
  9 + resource :articles do
  10 +
  11 + # Collect articles
  12 + #
  13 + # Parameters:
  14 + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created
  15 + # oldest - Collect the oldest articles. If nothing is passed the newest articles are collected
  16 + # limit - amount of articles returned. The default value is 20
  17 + #
  18 + # Example Request:
  19 + # GET host/api/v1/articles?from=2013-04-04-14:41:43&until=2015-04-04-14:41:43&limit=10&private_token=e96fff37c2238fdab074d1dcea8e6317
  20 + get do
  21 + present_articles(environment)
  22 + end
  23 +
  24 + desc "Return the article id"
  25 + get ':id' do
  26 + present_article(environment)
  27 + end
  28 +
  29 + get ':id/children' do
  30 + article = find_article(environment.articles, params[:id])
  31 +
  32 + #TODO make tests for this situation
  33 + votes_order = params.delete(:order) if params[:order]=='votes_score'
  34 + articles = select_filtered_collection_of(article, 'children', params)
  35 + articles = articles.display_filter(current_person, nil)
  36 +
  37 +
  38 + #TODO make tests for this situation
  39 + if votes_order
  40 + articles = articles.joins('left join votes on articles.id=votes.voteable_id').group('articles.id').reorder('sum(coalesce(votes.vote, 0)) DESC')
  41 + end
  42 +
  43 + Article.hit(articles)
  44 + present articles, :with => Entities::Article, :fields => params[:fields]
  45 + end
  46 +
  47 + get ':id/children/:child_id' do
  48 + article = find_article(environment.articles, params[:id])
  49 + present find_article(article.children, params[:child_id]), :with => Entities::Article, :fields => params[:fields]
  50 + end
  51 +
  52 + post ':id/children/suggest' do
  53 + parent_article = environment.articles.find(params[:id])
  54 +
  55 + suggest_article = SuggestArticle.new
  56 + suggest_article.article = params[:article]
  57 + suggest_article.article[:parent_id] = parent_article.id
  58 + suggest_article.target = parent_article.profile
  59 + suggest_article.requestor = current_person
  60 +
  61 + unless suggest_article.save
  62 + render_api_errors!(suggest_article.article_object.errors.full_messages)
  63 + end
  64 + present suggest_article, :with => Entities::Task, :fields => params[:fields]
  65 + end
  66 +
  67 + # Example Request:
  68 + # POST api/v1/articles/:id/children?private_token=234298743290432&article[name]=title&article[body]=body
  69 + post ':id/children' do
  70 +
  71 + parent_article = environment.articles.find(params[:id])
  72 + return forbidden! unless parent_article.allow_create?(current_person)
  73 +
  74 + klass_type= params[:content_type].nil? ? 'TinyMceArticle' : params[:content_type]
  75 + #FIXME see how to check the article types
  76 + #return forbidden! unless ARTICLE_TYPES.include?(klass_type)
  77 +
  78 + article = klass_type.constantize.new(params[:article])
  79 + article.parent = parent_article
  80 + article.last_changed_by = current_person
  81 + article.created_by= current_person
  82 + article.author= current_person
  83 + article.profile = parent_article.profile
  84 +
  85 + if !article.save
  86 + render_api_errors!(article.errors.full_messages)
  87 + end
  88 + present article, :with => Entities::Article, :fields => params[:fields]
  89 + end
  90 +
  91 + end
  92 +
  93 + kinds = %w[community person enterprise]
  94 + kinds.each do |kind|
  95 + resource kind.pluralize.to_sym do
  96 + segment "/:#{kind}_id" do
  97 + resource :articles do
  98 + get do
  99 + profile = environment.send(kind.pluralize).find(params["#{kind}_id"])
  100 + present_articles(profile)
  101 + end
  102 +
  103 + get ':id' do
  104 + profile = environment.send(kind.pluralize).find(params["#{kind}_id"])
  105 + present_article(profile)
  106 + end
  107 +
  108 + # Example Request:
  109 + # POST api/v1/{people,communities,enterprises}/:asset_id/articles?private_token=234298743290432&article[name]=title&article[body]=body
  110 + post do
  111 + profile = environment.send(kind.pluralize).find(params["#{kind}_id"])
  112 + post_article(profile, params)
  113 + end
  114 + end
  115 + end
  116 + end
  117 + end
  118 + end
  119 + end
  120 + end
  121 +end
... ...
lib/noosfero/api/v1/categories.rb 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +module Noosfero
  2 + module API
  3 + module V1
  4 + class Categories < Grape::API
  5 + before { authenticate! }
  6 +
  7 + resource :categories do
  8 +
  9 + get do
  10 + type = params[:category_type]
  11 + include_parent = params[:include_parent] == 'true'
  12 + include_children = params[:include_children] == 'true'
  13 +
  14 + categories = type.nil? ? environment.categories : environment.categories.where(:type => type)
  15 + present categories, :with => Entities::Category, parent: include_parent, children: include_children
  16 + end
  17 +
  18 + desc "Return the category by id"
  19 + get ':id' do
  20 + present environment.categories.find(params[:id]), :with => Entities::Category, parent: true, children: true
  21 + end
  22 +
  23 + end
  24 +
  25 + end
  26 + end
  27 + end
  28 +end
... ...
lib/noosfero/api/v1/comments.rb 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +module Noosfero
  2 + module API
  3 + module V1
  4 + class Comments < Grape::API
  5 + before { authenticate! }
  6 +
  7 + resource :articles do
  8 + # Collect comments from articles
  9 + #
  10 + # Parameters:
  11 + # reference_id - comment id used as reference to collect comment
  12 + # oldest - Collect the oldest comments from reference_id comment. If nothing is passed the newest comments are collected
  13 + # limit - amount of comments returned. The default value is 20
  14 + #
  15 + # Example Request:
  16 + # GET /articles/12/comments?oldest&limit=10&reference_id=23
  17 + get ":id/comments" do
  18 + article = find_article(environment.articles, params[:id])
  19 + comments = select_filtered_collection_of(article, :comments, params)
  20 +
  21 + present comments, :with => Entities::Comment
  22 + end
  23 +
  24 + get ":id/comments/:comment_id" do
  25 + article = find_article(environment.articles, params[:id])
  26 + present article.comments.find(params[:comment_id]), :with => Entities::Comment
  27 + end
  28 +
  29 + # Example Request:
  30 + # POST api/v1/articles/12/comments?private_token=2298743290432&body=new comment&title=New
  31 + post ":id/comments" do
  32 + article = find_article(environment.articles, params[:id])
  33 + options = params.select { |key,v| !['id','private_token'].include?(key) }.merge(:author => current_person)
  34 + present article.comments.create(options), :with => Entities::Comment
  35 + end
  36 + end
  37 +
  38 + end
  39 + end
  40 + end
  41 +end
... ...
lib/noosfero/api/v1/communities.rb 0 → 100644
... ... @@ -0,0 +1,73 @@
  1 +module Noosfero
  2 + module API
  3 + module V1
  4 + class Communities < Grape::API
  5 + before { authenticate! }
  6 +
  7 + resource :communities do
  8 +
  9 + # Collect comments from articles
  10 + #
  11 + # Parameters:
  12 + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created
  13 + # oldest - Collect the oldest comments from reference_id comment. If nothing is passed the newest comments are collected
  14 + # limit - amount of comments returned. The default value is 20
  15 + #
  16 + # Example Request:
  17 + # GET /communities?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10
  18 + # GET /communities?reference_id=10&limit=10&oldest
  19 + get do
  20 + communities = select_filtered_collection_of(environment, 'communities', params)
  21 + communities = communities.visible_for_person(current_person)
  22 + communities = communities.by_location(params) # Must be the last. May return Exception obj.
  23 + present communities, :with => Entities::Community
  24 + end
  25 +
  26 +
  27 + # Example Request:
  28 + # POST api/v1/communties?private_token=234298743290432&community[name]=some_name
  29 + post do
  30 + params[:community] ||= {}
  31 + begin
  32 + community = Community.create_after_moderation(current_person, params[:community].merge({:environment => environment}))
  33 + rescue
  34 + community = Community.new(params[:community])
  35 + end
  36 +
  37 + if !community.save
  38 + render_api_errors!(community.errors.full_messages)
  39 + end
  40 +
  41 + present community, :with => Entities::Community
  42 + end
  43 +
  44 + get ':id' do
  45 + community = environment.communities.visible_for_person(current_person).find_by_id(params[:id])
  46 + present community, :with => Entities::Community
  47 + end
  48 +
  49 + end
  50 +
  51 + resource :people do
  52 +
  53 + segment '/:person_id' do
  54 +
  55 + resource :communities do
  56 +
  57 + get do
  58 + person = environment.people.find(params[:person_id])
  59 + communities = select_filtered_collection_of(person, 'communities', params)
  60 + communities = communities.visible
  61 + present communities, :with => Entities::Community
  62 + end
  63 +
  64 + end
  65 +
  66 + end
  67 +
  68 + end
  69 +
  70 + end
  71 + end
  72 + end
  73 +end
... ...
lib/noosfero/api/v1/enterprises.rb 0 → 100644
... ... @@ -0,0 +1,58 @@
  1 +module Noosfero
  2 + module API
  3 + module V1
  4 + class Enterprises < Grape::API
  5 + before { authenticate! }
  6 +
  7 + resource :enterprises do
  8 +
  9 + # Collect enterprises from environment
  10 + #
  11 + # Parameters:
  12 + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created
  13 + # oldest - Collect the oldest comments from reference_id comment. If nothing is passed the newest comments are collected
  14 + # limit - amount of comments returned. The default value is 20
  15 + # georef params - read `Profile.by_location` for more information.
  16 + #
  17 + # Example Request:
  18 + # GET /enterprises?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10
  19 + # GET /enterprises?reference_id=10&limit=10&oldest
  20 + get do
  21 + enterprises = select_filtered_collection_of(environment, 'enterprises', params)
  22 + enterprises = enterprises.visible_for_person(current_person)
  23 + enterprises = enterprises.by_location(params) # Must be the last. May return Exception obj.
  24 + present enterprises, :with => Entities::Enterprise
  25 + end
  26 +
  27 + desc "Return one enterprise by id"
  28 + get ':id' do
  29 + enterprise = environment.enterprises.visible_for_person(current_person).find_by_id(params[:id])
  30 + present enterprise, :with => Entities::Enterprise
  31 + end
  32 +
  33 + end
  34 +
  35 + resource :people do
  36 +
  37 + segment '/:person_id' do
  38 +
  39 + resource :enterprises do
  40 +
  41 + get do
  42 + person = environment.people.find(params[:person_id])
  43 + enterprises = select_filtered_collection_of(person, 'enterprises', params)
  44 + enterprises = enterprises.visible.by_location(params)
  45 + present enterprises, :with => Entities::Enterprise
  46 + end
  47 +
  48 + end
  49 +
  50 + end
  51 +
  52 + end
  53 +
  54 +
  55 + end
  56 + end
  57 + end
  58 +end
... ...
lib/noosfero/api/v1/people.rb 0 → 100644
... ... @@ -0,0 +1,96 @@
  1 +module Noosfero
  2 + module API
  3 + module V1
  4 + class People < Grape::API
  5 + before { authenticate! }
  6 +
  7 + desc 'API Root'
  8 +
  9 + resource :people do
  10 +
  11 + # -- A note about privacy --
  12 + # We wold find people by location, but we must test if the related
  13 + # fields are public. We can't do it now, with SQL, while the location
  14 + # data and the fields_privacy are a serialized settings.
  15 + # We must build a new table for profile data, where we can set meta-data
  16 + # like:
  17 + # | id | profile_id | key | value | privacy_level | source |
  18 + # | 1 | 99 | city | Salvador | friends | user |
  19 + # | 2 | 99 | lng | -38.521 | me only | automatic |
  20 +
  21 + # Collect people from environment
  22 + #
  23 + # Parameters:
  24 + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created
  25 + # oldest - Collect the oldest comments from reference_id comment. If nothing is passed the newest comments are collected
  26 + # limit - amount of comments returned. The default value is 20
  27 + #
  28 + # Example Request:
  29 + # GET /people?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10
  30 + # GET /people?reference_id=10&limit=10&oldest
  31 +
  32 + desc "Find environment's people"
  33 + get do
  34 + people = select_filtered_collection_of(environment, 'people', params)
  35 + people = people.visible_for_person(current_person)
  36 + present people, :with => Entities::Person
  37 + end
  38 +
  39 + desc "Return the logged user information"
  40 + get "/me" do
  41 + present current_person, :with => Entities::Person
  42 + end
  43 +
  44 + desc "Return the person information"
  45 + get ':id' do
  46 + person = environment.people.visible_for_person(current_person).find_by_id(params[:id])
  47 + return not_found! if person.blank?
  48 + present person, :with => Entities::Person
  49 + end
  50 +
  51 + # Example Request:
  52 + # POST api/v1/people?person[login]=some_login&person[password]=some_password&person[name]=Jack
  53 + desc "Create person"
  54 + post do
  55 + user_data = {}
  56 + user_data[:login] = params[:person].delete(:login) || params[:person][:identifier]
  57 + user_data[:email] = params[:person].delete(:email)
  58 + user_data[:password] = params[:person].delete(:password)
  59 + user_data[:password_confirmation] = params[:person].delete(:password_confirmation)
  60 + user = User.build(user_data, params[:person], environment)
  61 + begin
  62 + user.signup!
  63 + rescue ActiveRecord::RecordInvalid
  64 + render_api_errors!(user.errors.full_messages)
  65 + end
  66 +
  67 + present user.person, :with => Entities::Person
  68 + end
  69 +
  70 + desc "Return the person friends"
  71 + get ':id/friends' do
  72 + person = environment.people.visible_for_person(current_person).find_by_id(params[:id])
  73 + return not_found! if person.blank?
  74 + friends = person.friends.visible
  75 + present friends, :with => Entities::Person
  76 + end
  77 +
  78 + desc "Return the person permissions on other profiles"
  79 + get ":id/permissions" do
  80 + person = environment.people.find(params[:id])
  81 + return not_found! if person.blank?
  82 + return forbidden! unless current_person == person || environment.admins.include?(current_person)
  83 +
  84 + output = {}
  85 + person.role_assignments.map do |role_assigment|
  86 + if role_assigment.resource.respond_to?(:identifier)
  87 + output[role_assigment.resource.identifier] = role_assigment.role.permissions
  88 + end
  89 + end
  90 + present output
  91 + end
  92 + end
  93 + end
  94 + end
  95 + end
  96 +end
... ...
lib/noosfero/api/v1/tasks.rb 0 → 100644
... ... @@ -0,0 +1,59 @@
  1 +module Noosfero
  2 + module API
  3 + module V1
  4 + class Tasks < Grape::API
  5 +# before { authenticate! }
  6 +
  7 +# ARTICLE_TYPES = Article.descendants.map{|a| a.to_s}
  8 +
  9 + resource :tasks do
  10 +
  11 + # Collect tasks
  12 + #
  13 + # Parameters:
  14 + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created
  15 + # oldest - Collect the oldest articles. If nothing is passed the newest articles are collected
  16 + # limit - amount of articles returned. The default value is 20
  17 + #
  18 + # Example Request:
  19 + # GET host/api/v1/tasks?from=2013-04-04-14:41:43&until=2015-04-04-14:41:43&limit=10&private_token=e96fff37c2238fdab074d1dcea8e6317
  20 + get do
  21 + tasks = select_filtered_collection_of(environment, 'tasks', params)
  22 + tasks = tasks.select {|t| current_person.has_permission?(t.permission, environment)}
  23 + present tasks, :with => Entities::Task, :fields => params[:fields]
  24 + end
  25 +
  26 + desc "Return the task id"
  27 + get ':id' do
  28 + task = find_task(environment, params[:id])
  29 + present task, :with => Entities::Task, :fields => params[:fields]
  30 + end
  31 + end
  32 +
  33 + kinds = %w[community person enterprise]
  34 + kinds.each do |kind|
  35 + resource kind.pluralize.to_sym do
  36 + segment "/:#{kind}_id" do
  37 + resource :tasks do
  38 + get do
  39 + profile = environment.send(kind.pluralize).find(params["#{kind}_id"])
  40 + present_tasks(profile)
  41 + end
  42 +
  43 + get ':id' do
  44 + profile = environment.send(kind.pluralize).find(params["#{kind}_id"])
  45 + present_task(profile)
  46 + end
  47 +
  48 + post do
  49 + profile = environment.send(kind.pluralize).find(params["#{kind}_id"])
  50 + post_task(profile, params)
  51 + end
  52 + end
  53 + end
  54 + end
  55 + end
  56 + end
  57 + end
  58 + end
  59 +end
... ...
lib/noosfero/geo_ref.rb
1 1 module Noosfero::GeoRef
2 2  
3   - KM_LAT = 111.2 # aproximate distance in km for 1 degree latitude
4   - KM_LNG = 85.3 # aproximate distance in km for 1 degree longitude
  3 + # May replace this module by http://www.postgresql.org/docs/9.3/static/earthdistance.html
  4 +
  5 + EARTH_RADIUS = 6378 # aproximate in km
  6 +
  7 + class << self
  8 +
  9 + def dist(lat1, lng1, lat2, lng2)
  10 + def deg2rad(d); (d*Math::PI)/180; end
  11 + def c(n); Math.cos(n); end
  12 + def s(n); Math.sin(n); end
  13 + lat1 = deg2rad lat1
  14 + lat2 = deg2rad lat2
  15 + dlng = deg2rad(lng2) - deg2rad(lng1)
  16 + EARTH_RADIUS * Math.atan2(
  17 + Math.sqrt(
  18 + ( c(lat2) * s(dlng) )**2 +
  19 + ( c(lat1) * s(lat2) - s(lat1) * c(lat2) * c(dlng) )**2
  20 + ),
  21 + s(lat1) * s(lat2) + c(lat1) * c(lat2) * c(dlng)
  22 + )
  23 + end
  24 +
  25 + # Write a SQL expression to return the distance from a profile to a
  26 + # reference point, in kilometers.
  27 + # http://www.plumislandmedia.net/mysql/vicenty-great-circle-distance-formula
  28 + def sql_dist(ref_lat, ref_lng)
  29 + "2*PI()*#{EARTH_RADIUS}*(
  30 + DEGREES(
  31 + ATAN2(
  32 + SQRT(
  33 + POW(COS(RADIANS(#{ref_lat}))*SIN(RADIANS(#{ref_lng}-lng)),2) +
  34 + POW(
  35 + COS(RADIANS(lat)) * SIN(RADIANS(#{ref_lat})) - (
  36 + SIN(RADIANS(lat)) * COS(RADIANS(#{ref_lat})) * COS(RADIANS(#{ref_lng}-lng))
  37 + ), 2
  38 + )
  39 + ),
  40 + SIN(RADIANS(lat)) * SIN(RADIANS(#{ref_lat})) +
  41 + COS(RADIANS(lat)) * COS(RADIANS(#{ref_lat})) * COS(RADIANS(#{ref_lng}-lng))
  42 + )
  43 + )/360
  44 + )"
  45 + end
  46 +
  47 + # Asks Google for the georef of a location.
  48 + def location_to_georef(location)
  49 + key = location.downcase
  50 + ll = Rails.cache.read key
  51 + return ll + [:CACHE] if ll.kind_of? Array
  52 + resp = RestClient.get 'https://maps.googleapis.com/maps/api/geocode/json?' +
  53 + 'sensor=false&address=' + url_encode(location)
  54 + if resp.nil? || resp.code.to_i != 200
  55 + if ENV['RAILS_ENV'] == 'test'
  56 + print " Google Maps API fail (code #{resp ? resp.code : :nil}) "
  57 + else
  58 + Rails.logger.warn "Google Maps API request information for " +
  59 + "\"#{location}\" fail. (code #{resp ? resp.code : :nil})"
  60 + end
  61 + return [ 0, 0, "HTTP_FAIL_#{resp.code}".to_sym ] # do not cache failed response
  62 + else
  63 + json = JSON.parse resp.body
  64 + if json && (r=json['results']) && (r=r[0]) && (r=r['geometry']) &&
  65 + (r=r['location']) && r['lat']
  66 + ll = [ r['lat'], r['lng'], :SUCCESS ]
  67 + else
  68 + status = json['status'] || 'Undefined Error'
  69 + message = "Google Maps API cant find \"#{location}\" (#{status})"
  70 + if ENV['RAILS_ENV'] == 'test'
  71 + print " #{message} "
  72 + else
  73 + Rails.logger.warn message
  74 + end
  75 + ll = [ 0, 0, status.to_sym ]
  76 + end
  77 + Rails.cache.write key, ll
  78 + end
  79 + ll
  80 + end
  81 +
  82 + end
5 83  
6 84 end
... ...
lib/noosfero/plugin.rb
... ... @@ -171,7 +171,6 @@ class Noosfero::Plugin
171 171 def all
172 172 @all ||= available_plugins.map{ |dir| (File.basename(dir) + "_plugin").camelize }
173 173 end
174   -
175 174 end
176 175  
177 176 def expanded_template(file_path, locals = {})
... ... @@ -241,6 +240,16 @@ class Noosfero::Plugin
241 240 nil
242 241 end
243 242  
  243 + # -> Customize the way comments are counted for Profiles and Environment
  244 + # considering more than just articles comments
  245 + # Used on statistic block
  246 + # Ex: a plugin may want that Communities receive comments themselves
  247 + # as evaluations
  248 + # returns = the number of comments to be sum on the statistics
  249 + def more_comments_count owner
  250 + nil
  251 + end
  252 +
244 253 # -> Adds tabs to the profile
245 254 # returns = { :title => title, :id => id, :content => content, :start => start }
246 255 # title = name that will be displayed.
... ...
lib/noosfero/plugin/parent_methods.rb
... ... @@ -54,6 +54,11 @@ class Noosfero::Plugin
54 54 File.exists?(File.join(root_path, 'controllers', "#{self.identifier}_admin_controller.rb"))
55 55 end
56 56  
  57 + # -> define grape class used to map resource api provided by the plugin
  58 + def api_mount_points
  59 + []
  60 + end
  61 +
57 62 def controllers
58 63 @controllers ||= Dir.glob("#{self.root_path}/controllers/*/*").map do |controller_file|
59 64 next unless controller_file =~ /_controller.rb$/
... ...
lib/noosfero/unicorn.rb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +# our defaults
  2 +listen "0.0.0.0:3000"
  3 +pid 'tmp/pids/unicorn.pid'
  4 +
  5 +preload_app true
  6 +GC.respond_to?(:copy_on_write_friendly=) and
  7 + GC.copy_on_write_friendly = true
  8 +
  9 +before_fork do |server, worker|
  10 + ActiveRecord::Base.connection.disconnect! if defined?(ActiveRecord::Base)
  11 +end
  12 +
  13 +after_fork do |server, worker|
  14 + ActiveRecord::Base.establish_connection if defined?(ActiveRecord::Base)
  15 +end
  16 +
  17 +# load local configuration file, if it exists
  18 +config = File.join(File.dirname(__FILE__), '../../config/unicorn.rb')
  19 +instance_eval(File.read(config), config) if File.exists?(config)
  20 +
... ...
lib/noosfero/version.rb
1 1 module Noosfero
2 2 PROJECT = 'noosfero'
3   - VERSION = '1.2'
  3 + VERSION = '1.3~0.0'
4 4 end
5 5  
6 6 root = File.expand_path(File.dirname(__FILE__) + '/../..')
... ...
lib/time_scopes.rb 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +module TimeScopes
  2 + def self.included(recipient)
  3 + recipient.extend(ClassMethods)
  4 + end
  5 +
  6 + module ClassMethods
  7 + def self.extended (base)
  8 + if base.respond_to?(:scope) && base.attribute_names.include?('created_at')
  9 + base.class_eval do
  10 + scope :younger_than, lambda { |created_at|
  11 + {:conditions => ["#{table_name}.created_at > ?", created_at]}
  12 + }
  13 +
  14 + scope :older_than, lambda { |created_at|
  15 + {:conditions => ["#{table_name}.created_at < ?", created_at]}
  16 + }
  17 + end
  18 + end
  19 + end
  20 + end
  21 +end
... ...
plugins/ldap/README.md
... ... @@ -13,6 +13,7 @@ Dependences
13 13 See the Noosfero install file. After install Noosfero, install LDAP dependences:
14 14  
15 15 $ gem install net-ldap -v 0.3.1
  16 +$ sudo apt-get install ruby-magic
16 17  
17 18 Enable Plugin
18 19 -------------
... ...
plugins/ldap/lib/ldap_authentication.rb
... ... @@ -19,6 +19,7 @@ require &#39;rubygems&#39;
19 19 require 'iconv'
20 20 require 'net/ldap'
21 21 require 'net/ldap/dn'
  22 +require 'magic'
22 23  
23 24 class LdapAuthentication
24 25  
... ... @@ -134,7 +135,18 @@ class LdapAuthentication
134 135  
135 136 def self.get_attr(entry, attr_name)
136 137 if !attr_name.blank?
137   - entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
  138 + val = entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
  139 + if val.nil?
  140 + Rails.logger.warn "LDAP entry #{entry.dn} has no attr #{attr_name}."
  141 + nil
  142 + elsif val == '' || val == ' '
  143 + Rails.logger.warn "LDAP entry #{entry.dn} has attr #{attr_name} empty."
  144 + ''
  145 + else
  146 + charset = Magic.guess_string_mime_encoding(val)
  147 + val.encode 'utf-8', charset, invalid: :replace, undef: :replace
  148 + end
138 149 end
139 150 end
  151 +
140 152 end
... ...
plugins/ldap/test/unit/ldap_authentication_test.rb
  1 +# encoding: UTF-8
1 2 require File.dirname(__FILE__) + '/../test_helper'
2 3  
3 4 class LdapAuthenticationTest < ActiveSupport::TestCase
4 5  
  6 + def pseudoEntry(data)
  7 + entry = data.clone
  8 + def entry.dn; 'testDN'; end
  9 + entry
  10 + end
  11 +
5 12 def setup
6 13 @ldap_config = load_ldap_config
7 14 end
8 15  
9   - should "host be nil as default" do
  16 + should 'host be nil as default' do
10 17 ldap = LdapAuthentication.new
11 18 assert_nil ldap.host
12 19 end
13 20  
14   - should "create with host passed as parameter" do
  21 + should 'create with host passed as parameter' do
15 22 value = 'http://myhost.com'
16 23 ldap = LdapAuthentication.new('host' => value)
17 24 assert_equal value, ldap.host
18 25 end
19 26  
20   - should "port be 389 as default" do
  27 + should 'port be 389 as default' do
21 28 ldap = LdapAuthentication.new
22 29 assert_equal 389, ldap.port
23 30 end
24 31  
25   - should "create with port passed as parameter" do
  32 + should 'create with port passed as parameter' do
26 33 value = 555
27 34 ldap = LdapAuthentication.new('port' => value)
28 35 assert_equal value, ldap.port
29 36 end
30 37  
31   - should "account be nil as default" do
  38 + should 'account be nil as default' do
32 39 ldap = LdapAuthentication.new
33 40 assert_nil ldap.account
34 41 end
35 42  
36   - should "create with account passed as parameter" do
  43 + should 'create with account passed as parameter' do
37 44 value = 'uid=sector,ou=Service,ou=corp,dc=company,dc=com,dc=br'
38 45 ldap = LdapAuthentication.new('account' => value)
39 46 assert_equal value, ldap.account
40 47 end
41 48  
42   - should "account_password be nil as default" do
  49 + should 'account_password be nil as default' do
43 50 ldap = LdapAuthentication.new
44 51 assert_nil ldap.account_password
45 52 end
46 53  
47   - should "create with account_password passed as parameter" do
  54 + should 'create with account_password passed as parameter' do
48 55 value = 'password'
49 56 ldap = LdapAuthentication.new('account_password' => value)
50 57 assert_equal value, ldap.account_password
51 58 end
52 59  
53   - should "base_dn be nil as default" do
  60 + should 'base_dn be nil as default' do
54 61 ldap = LdapAuthentication.new
55 62 assert_nil ldap.base_dn
56 63 end
57 64  
58   - should "create with base_dn passed as parameter" do
  65 + should 'create with base_dn passed as parameter' do
59 66 value = 'dc=company,dc=com,dc=br'
60 67 ldap = LdapAuthentication.new('base_dn' => value)
61 68 assert_equal value, ldap.base_dn
62 69 end
63 70  
64   - should "attr_login be nil as default" do
  71 + should 'attr_login be nil as default' do
65 72 ldap = LdapAuthentication.new
66 73 assert_nil ldap.attr_login
67 74 end
68 75  
69   - should "create with attr_login passed as parameter" do
  76 + should 'create with attr_login passed as parameter' do
70 77 value = 'uid'
71 78 ldap = LdapAuthentication.new('attr_login' => value)
72 79 assert_equal value, ldap.attr_login
73 80 end
74 81  
75   - should "attr_fullname be nil as default" do
  82 + should 'attr_fullname be nil as default' do
76 83 ldap = LdapAuthentication.new
77 84 assert_nil ldap.attr_fullname
78 85 end
79 86  
80   - should "create with attr_fullname passed as parameter" do
  87 + should 'create with attr_fullname passed as parameter' do
81 88 value = 'Noosfero System'
82 89 ldap = LdapAuthentication.new('attr_fullname' => value)
83 90 assert_equal value, ldap.attr_fullname
84 91 end
85 92  
86   - should "attr_mail be nil as default" do
  93 + should 'attr_mail be nil as default' do
87 94 ldap = LdapAuthentication.new
88 95 assert_nil ldap.attr_mail
89 96 end
90 97  
91   - should "create with attr_mail passed as parameter" do
  98 + should 'create with attr_mail passed as parameter' do
92 99 value = 'test@noosfero.com'
93 100 ldap = LdapAuthentication.new('attr_mail' => value)
94 101 assert_equal value, ldap.attr_mail
95 102 end
96 103  
97   - should "onthefly_register be false as default" do
  104 + should 'onthefly_register be false as default' do
98 105 ldap = LdapAuthentication.new
99 106 refute ldap.onthefly_register
100 107 end
101 108  
102   - should "create with onthefly_register passed as parameter" do
  109 + should 'create with onthefly_register passed as parameter' do
103 110 value = true
104 111 ldap = LdapAuthentication.new('onthefly_register' => value)
105 112 assert_equal value, ldap.onthefly_register
106 113 end
107 114  
108   - should "filter be nil as default" do
  115 + should 'filter be nil as default' do
109 116 ldap = LdapAuthentication.new
110 117 assert_nil ldap.filter
111 118 end
112 119  
113   - should "create with filter passed as parameter" do
  120 + should 'create with filter passed as parameter' do
114 121 value = 'test'
115 122 ldap = LdapAuthentication.new('filter' => value)
116 123 assert_equal value, ldap.filter
117 124 end
118 125  
119   - should "tls be false as default" do
  126 + should 'tls be false as default' do
120 127 ldap = LdapAuthentication.new
121 128 refute ldap.tls
122 129 end
123 130  
124   - should "create with tls passed as parameter" do
  131 + should 'create with tls passed as parameter' do
125 132 value = true
126 133 ldap = LdapAuthentication.new('tls' => value)
127 134 assert_equal value, ldap.tls
128 135 end
129 136  
130   - should "onthefly_register? return true if onthefly_register is true" do
  137 + should 'onthefly_register? return true if onthefly_register is true' do
131 138 ldap = LdapAuthentication.new('onthefly_register' => true)
132 139 assert ldap.onthefly_register?
133 140 end
134 141  
135   - should "onthefly_register? return false if onthefly_register is false" do
  142 + should 'onthefly_register? return false if onthefly_register is false' do
136 143 ldap = LdapAuthentication.new('onthefly_register' => false)
137 144 refute ldap.onthefly_register?
138 145 end
139 146  
  147 + should 'detect and convert non utf-8 charset from ldap' do
  148 + entry = pseudoEntry('name' => "Jos\xE9 Jo\xE3o")
  149 + name = LdapAuthentication.get_attr entry, 'name'
  150 + assert_equal name, 'José João'
  151 + end
  152 +
  153 + should 'dont crash when entry key is empty string' do
  154 + entry = pseudoEntry('name' => "")
  155 + name = LdapAuthentication.get_attr entry, 'name'
  156 + assert_equal name, ''
  157 + end
  158 +
  159 + should 'dont crash when entry key has only a space char' do
  160 + entry = pseudoEntry('name' => " ")
  161 + name = LdapAuthentication.get_attr entry, 'name'
  162 + assert_equal name, ''
  163 + end
  164 +
  165 + should 'dont crash when entry key is nil' do
  166 + entry = pseudoEntry('name' => nil)
  167 + name = LdapAuthentication.get_attr entry, 'name'
  168 + assert_equal name, nil
  169 + end
  170 +
  171 + should 'dont crash when entry key does not exists' do
  172 + entry = pseudoEntry({})
  173 + name = LdapAuthentication.get_attr entry, 'name'
  174 + assert_equal name, nil
  175 + end
  176 +
140 177 if ldap_configured?
141 178 should 'return the user attributes' do
142 179 auth = LdapAuthentication.new(@ldap_config['server'])
... ...
plugins/organization_ratings/controllers/organization_ratings_plugin_admin_controller.rb 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +class OrganizationRatingsPluginAdminController < PluginAdminController
  2 +
  3 + include RatingsHelper
  4 + helper :ratings
  5 + append_view_path File.join(File.dirname(__FILE__) + '/../views')
  6 +
  7 + def index
  8 + end
  9 +
  10 + def update
  11 + if env_organization_ratings_config.update_attributes(params[:organization_ratings_config])
  12 + session[:notice] = _('Configuration updated successfully.')
  13 + else
  14 + session[:notice] = _('Configuration could not be saved.')
  15 + end
  16 + render :action => 'index'
  17 + end
  18 +
  19 +end
0 20 \ No newline at end of file
... ...
plugins/organization_ratings/controllers/organization_ratings_plugin_profile_controller.rb 0 → 100644
... ... @@ -0,0 +1,76 @@
  1 +class OrganizationRatingsPluginProfileController < ProfileController
  2 + include RatingsHelper
  3 + helper :ratings
  4 +
  5 + def new_rating
  6 + @rating_available = user_can_rate_now?
  7 + @users_ratings = get_ratings(profile.id).paginate(
  8 + :per_page => env_organization_ratings_config.per_page,
  9 + :page => params[:npage]
  10 + )
  11 + if request.post?
  12 + if @rating_available
  13 + create_new_rate
  14 + else
  15 + session[:notice] = _("You can not vote on this %s") % profile.class.name
  16 + end
  17 + end
  18 + end
  19 +
  20 + private
  21 +
  22 + def user_can_rate_now?
  23 + return false unless user
  24 + ratings = OrganizationRating.where(
  25 + :organization_id => profile.id,
  26 + :person_id => user.id
  27 + )
  28 +
  29 + return false if (!ratings.empty? && env_organization_ratings_config.vote_once)
  30 +
  31 + if ratings.empty?
  32 + true
  33 + else
  34 + elapsed_time_since_last_rating = Time.zone.now - ratings.last.created_at
  35 + elapsed_time_since_last_rating > env_organization_ratings_config.cooldown.hours
  36 + end
  37 + end
  38 +
  39 + def create_new_rate
  40 + rating = OrganizationRating.new(params[:organization_rating])
  41 + rating.person = current_user.person
  42 + rating.organization = profile
  43 + rating.value = params[:organization_rating_value] if params[:organization_rating_value]
  44 +
  45 + if rating.save
  46 + create_rating_comment(rating)
  47 + session[:notice] = _("%s successfully rated!") % profile.name
  48 + else
  49 + session[:notice] = _("Sorry, there were problems rating this profile.")
  50 + end
  51 +
  52 + redirect_to :controller => 'profile', :action => 'index'
  53 + end
  54 +
  55 + def create_rating_comment(rating)
  56 + if params[:comments]
  57 + comment_task = CreateOrganizationRatingComment.create!(
  58 + params[:comments].merge(
  59 + :requestor => rating.person,
  60 + :organization_rating_id => rating.id,
  61 + :target => rating.organization
  62 + )
  63 + )
  64 + comment_task.finish if can_perform?(params)
  65 + end
  66 + end
  67 +
  68 + def can_perform? (params)
  69 + (params[:comments][:body].blank? ||
  70 + !env_organization_ratings_config.are_moderated)
  71 + end
  72 +
  73 + def permission
  74 + :manage_memberships
  75 + end
  76 +end
... ...
plugins/organization_ratings/db/migrate/20150830225546_create_organization_ratings.rb 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +class CreateOrganizationRatings < ActiveRecord::Migration
  2 + def change
  3 + create_table :organization_ratings do |t|
  4 + t.belongs_to :organization
  5 + t.belongs_to :person
  6 + t.belongs_to :comment
  7 + t.integer :value
  8 +
  9 + t.timestamps
  10 + end
  11 + end
  12 +end
... ...
plugins/organization_ratings/db/migrate/20150830225733_create_organization_ratings_config.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +class CreateOrganizationRatingsConfig < ActiveRecord::Migration
  2 +
  3 + def change
  4 + create_table :organization_ratings_configs do |t|
  5 + t.belongs_to :environment
  6 + t.integer :cooldown, :integer, :default => 24
  7 + t.integer :default_rating, :integer, :default => 1
  8 + t.string :order, :string, :default => "recent"
  9 + t.integer :per_page, :integer, :default => 10
  10 + t.boolean :vote_once, :boolean, :default => false
  11 + t.boolean :are_moderated, :boolean, :default => true
  12 + end
  13 + end
  14 +end
... ...
plugins/organization_ratings/db/migrate/20150830230047_add_comments_count_to_profile.rb 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +class AddCommentsCountToProfile < ActiveRecord::Migration
  2 + def self.up
  3 + change_table :profiles do |t|
  4 + t.integer :comments_count
  5 + end
  6 + end
  7 +
  8 + def self.down
  9 + remove_column :profiles, :comments_count
  10 + end
  11 +end
0 12 \ No newline at end of file
... ...
plugins/organization_ratings/features/rate_community.feature 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +Feature: rate_community
  2 + As a user
  3 + I want to be able rate a community
  4 + So that users can see my feedback about that community
  5 +
  6 + Background:
  7 + Given plugin "OrganizationRatings" is enabled on environment
  8 + Given the following user
  9 + | login | name |
  10 + | joaosilva | Joao Silva |
  11 + And the following community
  12 + | identifier | name |
  13 + | mycommunity | My Community |
  14 + And the following blocks
  15 + | owner | type |
  16 + | mycommunity | AverageRatingBlock |
  17 + | mycommunity | OrganizationRatingsBlock |
  18 + And the environment domain is "localhost"
  19 + And I am logged in as "joaosilva"
  20 +
  21 + @selenium
  22 + Scenario: display rate button inside average block
  23 + Given I am on mycommunity's homepage
  24 + Then I should see "Rate this Community" within ".average-rating-block"
  25 + And I should see "Be the first to rate" within ".average-rating-block"
  26 +
  27 + @selenium
  28 + Scenario: display rate button inside communities ratings block
  29 + Given I am on mycommunity's homepage
  30 + Then I should see "Rate Community" within ".make-report-block"
... ...
plugins/organization_ratings/lib/average_rating_block.rb 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +class AverageRatingBlock < Block
  2 + include RatingsHelper
  3 +
  4 + def self.description
  5 + _('Organization Average Rating')
  6 + end
  7 +
  8 + def help
  9 + _('This block displays the organization average rating.')
  10 + end
  11 +
  12 + def content(args = {})
  13 + profile_identifier = self.owner.identifier
  14 + average_rating = OrganizationRating.average_rating self.owner.id
  15 +
  16 + proc do
  17 + render(
  18 + :file => 'blocks/display_organization_average_rating',
  19 + :locals => {
  20 + :profile_identifier => profile_identifier,
  21 + :average_rating => average_rating
  22 + }
  23 + )
  24 + end
  25 + end
  26 +
  27 + def cacheable?
  28 + false
  29 + end
  30 +end
... ...
plugins/organization_ratings/lib/create_organization_rating_comment.rb 0 → 100644
... ... @@ -0,0 +1,131 @@
  1 +class CreateOrganizationRatingComment < Task
  2 + include Rails.application.routes.url_helpers
  3 +
  4 + validates_presence_of :requestor_id, :organization_rating_id, :target_id
  5 +
  6 + settings_items :organization_rating_id, :type => Integer, :default => nil
  7 + settings_items :organization_rating_comment_id, :type => Integer, :default => nil
  8 +
  9 + attr_accessible :organization_rating_id, :body, :requestor
  10 + attr_accessible :reject_explanation, :target
  11 +
  12 + before_save :update_comment_body
  13 +
  14 + DATA_FIELDS = ['body']
  15 + DATA_FIELDS.each do |field|
  16 + settings_items field.to_sym
  17 + end
  18 +
  19 + def update_comment_body
  20 + if self.organization_rating_comment_id.nil?
  21 + create_comment
  22 + else
  23 + comment = Comment.find_by_id(self.organization_rating_comment_id)
  24 + comment.body = get_comment_message
  25 + comment.save
  26 + end
  27 + end
  28 +
  29 + def create_comment
  30 + if (self.body && !self.body.blank?)
  31 + comment_body = _("Comment waiting for approval")
  32 + comment = Comment.create!(:source => self.target, :body => comment_body, :author => self.requestor)
  33 +
  34 +
  35 + self.organization_rating_comment_id = comment.id
  36 + link_comment_with_its_rating(comment)
  37 + end
  38 + end
  39 +
  40 + def link_comment_with_its_rating(user_comment)
  41 + rating = OrganizationRating.find(self.organization_rating_id)
  42 + rating.comment = user_comment
  43 + rating.save
  44 + end
  45 +
  46 + def get_comment_message
  47 + if self.status == Status::CANCELLED
  48 + _("Comment rejected")
  49 + elsif self.status == Status::FINISHED
  50 + self.body
  51 + else
  52 + _("No comment")
  53 + end
  54 + end
  55 +
  56 + def accept_details
  57 + true
  58 + end
  59 +
  60 + def title
  61 + _("New Comment")
  62 + end
  63 +
  64 + def information
  65 + message = _("<a href=%{requestor_url}>%{requestor}</a> wants to create a comment in this %{target_class}") %
  66 + {:requestor_url => url_for(self.requestor.url), :requestor => self.requestor.name, :target_class => self.target.class.name.downcase}
  67 +
  68 + {:message => message}
  69 + end
  70 +
  71 + def reject_details
  72 + true
  73 + end
  74 +
  75 + def icon
  76 + {:type => :profile_image, :profile => requestor, :url => requestor.url}
  77 + end
  78 +
  79 + # tells if this request was rejected
  80 + def rejected?
  81 + self.status == Task::Status::CANCELLED
  82 + end
  83 +
  84 + # tells if this request was appoved
  85 + def approved?
  86 + self.status == Task::Status::FINISHED
  87 + end
  88 +
  89 + def target_notification_description
  90 + _("%{requestor} wants to create a comment in this \"%{target}\"") %
  91 + {:requestor => self.requestor.name, :target => self.target.class.name.downcase }
  92 + end
  93 +
  94 + def target_notification_message
  95 + _("User \"%{user}\" just requested to create a comment in the %{target_class}
  96 + \"%{target_name}\".
  97 + You have to approve or reject it through the \"Pending Validations\"
  98 + section in your control panel.\n") %
  99 + { :user => self.requestor.name, :target_class => self.target.class.name.downcase, :target_name => self.target.name }
  100 + end
  101 +
  102 + def task_created_message
  103 + _("Your request for commenting at %{target} was
  104 + just sent. Environment administrator will receive it and will approve or
  105 + reject your request according to his methods and criteria.
  106 + You will be notified as soon as environment administrator has a position
  107 + about your request.") %
  108 + { :target => self.target.name }
  109 + end
  110 +
  111 + def task_cancelled_message
  112 + _("Your request for commenting at %{target} was
  113 + not approved by the environment administrator. The following explanation
  114 + was given: \n\n%{explanation}") %
  115 + { :target => self.target.name,
  116 + :explanation => self.reject_explanation }
  117 + end
  118 +
  119 + def task_finished_message
  120 + _('Your request for commenting was approved.
  121 + You can access %{url} to see your comment.') %
  122 + { :url => ratings_url }
  123 + end
  124 +
  125 + private
  126 +
  127 + def ratings_url
  128 + url = url_for(self.target.public_profile_url) + "/plugin/organization_ratings/new_rating"
  129 + end
  130 +
  131 +end
... ...
plugins/organization_ratings/lib/ext/comments.rb 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +require_dependency "comment"
  2 +
  3 +Comment.class_eval do
  4 +
  5 + has_one :organization_rating
  6 +end
... ...
plugins/organization_ratings/lib/ext/environment.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +require_dependency "environment"
  2 +
  3 +class Environment
  4 + has_one :organization_ratings_config
  5 +end
... ...
plugins/organization_ratings/lib/ext/organization.rb 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +require_dependency 'organization'
  2 +
  3 +Organization.class_eval do
  4 + has_many :organization_ratings
  5 +
  6 + has_many :comments, :class_name => 'Comment', :foreign_key => 'source_id', :dependent => :destroy, :order => 'created_at asc'
  7 +end
... ...
plugins/organization_ratings/lib/ext/person.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +require_dependency 'person'
  2 +
  3 +Person.class_eval do
  4 + has_many :organization_ratings
  5 +end
... ...
plugins/organization_ratings/lib/organization_rating.rb 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +class OrganizationRating < ActiveRecord::Base
  2 + belongs_to :person
  3 + belongs_to :organization
  4 + belongs_to :comment
  5 +
  6 + attr_accessible :value, :person, :organization, :comment
  7 +
  8 + validates :value,
  9 + :presence => true, :inclusion => {
  10 + in: 1..5, message: _("must be between 1 and 5")
  11 + }
  12 +
  13 + validates :organization_id, :person_id,
  14 + :presence => true
  15 +
  16 +
  17 + def self.average_rating organization_id
  18 + average = OrganizationRating.where(organization_id: organization_id).average(:value)
  19 +
  20 + if average
  21 + (average - average.truncate) >= 0.5 ? average.ceil : average.floor
  22 + end
  23 + end
  24 +
  25 +end
... ...
plugins/organization_ratings/lib/organization_ratings_block.rb 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +class OrganizationRatingsBlock < Block
  2 + include RatingsHelper
  3 +
  4 + def self.description
  5 + _('Organization Ratings')
  6 + end
  7 +
  8 + def help
  9 + _('This block displays the community ratings.')
  10 + end
  11 +
  12 + def content(args = {})
  13 + block = self
  14 +
  15 + proc do
  16 + render(
  17 + :file => 'blocks/organization_ratings_block',
  18 + :locals => {:block => block}
  19 + )
  20 + end
  21 + end
  22 +
  23 + def limit_number_of_ratings
  24 + env_organization_ratings_config.per_page
  25 + end
  26 +
  27 + def cacheable?
  28 + false
  29 + end
  30 +end
... ...
plugins/organization_ratings/lib/organization_ratings_config.rb 0 → 100644
... ... @@ -0,0 +1,53 @@
  1 +class OrganizationRatingsConfig < ActiveRecord::Base
  2 +
  3 + belongs_to :environment
  4 +
  5 + attr_accessible :cooldown, :default_rating, :order, :per_page
  6 + attr_accessible :vote_once, :are_moderated, :environment_id
  7 +
  8 + ORDER_OPTIONS = {recent: _('More Recent'), best: _('Best Ratings')}
  9 +
  10 + MINIMUM_RATING = 1
  11 + MAX_COOLDOWN = 1000
  12 +
  13 + validates :default_rating,
  14 + :presence => true, :numericality => {
  15 + greater_than_or_equal_to: MINIMUM_RATING,
  16 + less_than_or_equal_to: 5
  17 + }
  18 +
  19 + validates :cooldown,
  20 + :presence => true, :numericality => {
  21 + greater_than_or_equal_to: 0,
  22 + less_than_or_equal_to: MAX_COOLDOWN
  23 + }
  24 +
  25 + validates :per_page,
  26 + :presence => true, :numericality => {
  27 + :greater_than_or_equal_to => 5,
  28 + :less_than_or_equal_to => 20
  29 + }
  30 +
  31 +
  32 + def order_options
  33 + ORDER_OPTIONS
  34 + end
  35 +
  36 + def minimum_ratings
  37 + MINIMUM_RATING
  38 + end
  39 +
  40 + def max_cooldown
  41 + MAX_COOLDOWN
  42 + end
  43 +
  44 + class << self
  45 + def instance
  46 + environment = Environment.default
  47 + environment.organization_ratings_config || create(environment_id: environment.id)
  48 + end
  49 +
  50 + private :new
  51 + end
  52 +
  53 +end
... ...
plugins/organization_ratings/lib/organization_ratings_plugin.rb 0 → 100644
... ... @@ -0,0 +1,73 @@
  1 +class OrganizationRatingsPlugin < Noosfero::Plugin
  2 + include Noosfero::Plugin::HotSpot
  3 +
  4 + def self.plugin_name
  5 + "Organization Ratings"
  6 + end
  7 +
  8 + def self.plugin_description
  9 + _("A plugin that allows you to rate a organization and comment about it.")
  10 + end
  11 +
  12 + module Hotspots
  13 + def organization_ratings_plugin_comments_extra_fields
  14 + nil
  15 + end
  16 +
  17 + def organization_ratings_title
  18 + nil
  19 + end
  20 +
  21 + def organization_ratings_plugin_star_message
  22 + nil
  23 + end
  24 +
  25 + def organization_ratings_plugin_extra_fields_show_data user_rating
  26 + nil
  27 + end
  28 + end
  29 +
  30 + # Plugin Hotspot to display the average rating
  31 + def display_organization_average_rating organization
  32 + unless organization.nil?
  33 + average_rating = OrganizationRating.average_rating organization.id
  34 +
  35 + Proc::new {
  36 + render :file => 'blocks/display_organization_average_rating',
  37 + :locals => {
  38 + :profile_identifier => organization.identifier,
  39 + :average_rating => average_rating
  40 + }
  41 + }
  42 + end
  43 + end
  44 +
  45 + def more_comments_count owner
  46 + if owner.kind_of?(Environment) then
  47 + owner.profiles.sum(:comments_count)
  48 + elsif owner.kind_of?(Profile) then
  49 + owner.comments_count
  50 + else
  51 + 0
  52 + end
  53 + end
  54 +
  55 + def self.extra_blocks
  56 + {
  57 + OrganizationRatingsBlock => {:type => [Enterprise, Community], :position => ['1']},
  58 + AverageRatingBlock => {:type => [Enterprise, Community]}
  59 + }
  60 + end
  61 +
  62 + def stylesheet?
  63 + true
  64 + end
  65 +
  66 + def js_files
  67 + %w(
  68 + public/rate.js
  69 + public/comunities_rating_management.js
  70 + )
  71 + end
  72 +
  73 +end
... ...
plugins/organization_ratings/lib/ratings_helper.rb 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +module RatingsHelper
  2 +
  3 + def env_organization_ratings_config
  4 + OrganizationRatingsConfig.instance
  5 + end
  6 +
  7 + def get_ratings (profile_id)
  8 + order_options = env_organization_ratings_config.order_options
  9 + if env_organization_ratings_config.order.downcase == order_options[:recent]
  10 + ratings = OrganizationRating.where(organization_id: profile_id).order("value DESC")
  11 + else
  12 + ratings = OrganizationRating.where(organization_id: profile_id).order("created_at DESC")
  13 + end
  14 + end
  15 +end
0 16 \ No newline at end of file
... ...
plugins/organization_ratings/public/images/small-star-negative.png 0 → 100644

529 Bytes