Commit b83780786c4695c818f79fc8800c6636a74b000b

Authored by Victor Costa
2 parents 9176e687 8e288dc4

Merge branch 'api' into staging

Conflicts:
	lib/noosfero/api/v1/articles.rb
	test/unit/api/articles_test.rb
Gemfile
... ... @@ -23,6 +23,10 @@ gem 'eita-jrails', '~> 0.9.5', require: 'jrails'
23 23 # API dependencies
24 24 gem 'grape', '~> 0.12'
25 25 gem 'grape-entity'
  26 +gem 'grape-swagger'
  27 +gem 'swagger-ui_rails'
  28 +gem 'kramdown'
  29 +
26 30 #FIXME Get the Grape Loggin from master yo solve this issue https://github.com/intridea/grape/issues/1059
27 31 #We have to remove this commit referenve code when update the next release of grape_logging. Actualy we are using (1.1.2)
28 32 gem 'grape_logging', :git => 'https://github.com/aceunreal/grape_logging.git', :ref => 'f1755ae'
... ...
app/controllers/api_docs_controller.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +class ApiDocsController < ApplicationController
  2 + layout :api_layout
  3 +
  4 + private
  5 + def api_layout
  6 + params[:controller]
  7 + end
  8 +end
... ...
app/views/api_docs/index.html.erb 0 → 100644
... ... @@ -0,0 +1 @@
  1 +<%= render 'swagger_ui/swagger_ui', discovery_url: '/api/v1/api_docs' %>
... ...
app/views/layouts/api_docs.html.erb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +<!DOCTYPE html>
  2 +<html>
  3 + <head>
  4 + <meta charset="utf-8">
  5 + <title></title>
  6 + <%= stylesheet_link_tag 'swagger-ui' %>
  7 + <%= javascript_include_tag 'swagger-ui' %>
  8 + </head>
  9 + <body>
  10 + <div id="content">
  11 + <%= yield %>
  12 + </div>
  13 + </body>
  14 +</html>
... ...
config/routes.rb
... ... @@ -23,6 +23,7 @@ Noosfero::Application.routes.draw do
23 23  
24 24 match 'site(/:action)', :controller => 'home'
25 25 match 'api(/:action)', :controller => 'api'
  26 + match 'api_docs(/:action)', :controller => 'api_docs'
26 27  
27 28 match 'images(/*stuff)' => 'not_found#nothing'
28 29 match 'stylesheets(/*stuff)' => 'not_found#nothing'
... ...
lib/noosfero/api/api.rb
... ... @@ -54,6 +54,8 @@ module Noosfero
54 54  
55 55 mount Session
56 56  
  57 + add_swagger_documentation api_version: 'v1', mount_path: '/api_docs', markdown: GrapeSwagger::Markdown::KramdownAdapter unless Rails.env.production?
  58 +
57 59 # hook point which allow plugins to add Grape::API extensions to API::API
58 60 #finds for plugins which has api mount points classes defined (the class should extends Grape::API)
59 61 @plugins = Noosfero::Plugin.all.map { |p| p.constantize }
... ...
lib/noosfero/api/entities.rb
... ... @@ -64,7 +64,7 @@ module Noosfero
64 64  
65 65 class Person < Profile
66 66 root 'people', 'person'
67   - expose :user, :using => UserBasic
  67 + expose :user, :using => UserBasic, documentation: {type: 'User', desc: 'The user data of a person' }
68 68 end
69 69  
70 70 class Enterprise < Profile
... ... @@ -80,11 +80,11 @@ module Noosfero
80 80 root 'articles', 'article'
81 81 expose :id
82 82 expose :body
83   - expose :abstract
  83 + expose :abstract, documentation: {type: 'String', desc: 'Teaser of the body'}
84 84 expose :created_at, :format_with => :timestamp
85 85 expose :title, :documentation => {:type => "String", :desc => "Title of the article"}
86   - expose :created_by, :as => :author, :using => Profile
87   - expose :profile, :using => Profile
  86 + expose :created_by, :as => :author, :using => Profile, :documentation => {type: 'Profile', desc: 'The profile author that create the article'}
  87 + expose :profile, :using => Profile, :documentation => {type: 'Profile', desc: 'The profile associated with the article'}
88 88 expose :categories, :using => Category
89 89 expose :image, :using => Image
90 90 #TODO Apply vote stuff in core and make this test
... ... @@ -94,7 +94,7 @@ module Noosfero
94 94 expose :position
95 95 expose :hits
96 96 expose :start_date
97   - expose :end_date
  97 + expose :end_date, :documentation => {type: 'DateTime', desc: 'The date of finish of the article'}
98 98 expose :tag_list
99 99 expose :children_count
100 100 end
... ... @@ -105,6 +105,7 @@ module Noosfero
105 105 expose :children, using: ArticleBase do |article, options|
106 106 article.children.limit(Noosfero::API::V1::Articles::MAX_PER_PAGE)
107 107 end
  108 + expose :slug, :documentation => {:type => "String", :desc => "Trimmed and parsed name of a article"}
108 109 end
109 110  
110 111 class Comment < Entity
... ... @@ -134,7 +135,7 @@ module Noosfero
134 135 end
135 136  
136 137 class UserLogin < User
137   - expose :private_token
  138 + expose :private_token, documentation: {type: 'String', desc: 'A valid authentication code for post/delete api actions'}
138 139 end
139 140  
140 141 class Task < Entity
... ...
lib/noosfero/api/v1/articles.rb
... ... @@ -21,6 +21,19 @@ module Noosfero
21 21 # Example Request:
22 22 # GET host/api/v1/articles?from=2013-04-04-14:41:43&until=2015-04-04-14:41:43&limit=10&private_token=e96fff37c2238fdab074d1dcea8e6317
23 23  
  24 + desc 'Return all articles of all kinds' do
  25 + detail 'Get all articles filtered by fields in query params'
  26 + params Noosfero::API::Entities::Article.documentation
  27 + success Noosfero::API::Entities::Article
  28 + failure [[403, 'Forbidden']]
  29 + named 'ArticlesList'
  30 + headers [
  31 + 'Per-Page' => {
  32 + description: 'Total number of records',
  33 + required: false
  34 + }
  35 + ]
  36 + end
24 37 get do
25 38 present_articles(environment)
26 39 end
... ... @@ -30,8 +43,14 @@ module Noosfero
30 43 present_articles(current_person, 'following_articles')
31 44 end
32 45  
33   - desc "Return the article id"
34   - get ':id' do
  46 + desc "Return one article by id" do
  47 + detail 'Get only one article by id. If not found the "forbidden" http error is showed'
  48 + params Noosfero::API::Entities::Article.documentation
  49 + success Noosfero::API::Entities::Article
  50 + failure [[403, 'Forbidden']]
  51 + named 'ArticleById'
  52 + end
  53 + get ':id', requirements: {id: /[0-9]+/} do
35 54 present_article(environment)
36 55 end
37 56  
... ... @@ -42,6 +61,12 @@ module Noosfero
42 61 present article, :with => Entities::Article, :fields => params[:fields]
43 62 end
44 63  
  64 + desc 'Report a abuse and/or violent content in a article by id' do
  65 + detail 'Submit a abuse (in general, a content violation) report about a specific article'
  66 + params Noosfero::API::Entities::Article.documentation
  67 + failure [[400, 'Bad Request']]
  68 + named 'ArticleReportAbuse'
  69 + end
45 70 post ':id/report_abuse' do
46 71 article = find_article(environment.articles, params[:id])
47 72 profile = article.profile
... ... @@ -70,14 +95,23 @@ module Noosfero
70 95  
71 96 end
72 97  
73   - desc "Returns the total followers for the article"
  98 + desc "Returns the total followers for the article" do
  99 + detail 'Get the followers of a specific article by id'
  100 + failure [[403, 'Forbidden']]
  101 + named 'ArticleFollowers'
  102 + end
74 103 get ':id/followers' do
75 104 article = find_article(environment.articles, params[:id])
76 105 total = article.person_followers.count
77 106 {:total_followers => total}
78 107 end
79 108  
80   - desc "Add a follower for the article"
  109 + desc "Add a follower for the article" do
  110 + detail 'Add the current user identified by private token, like a follower of a article'
  111 + params Noosfero::API::Entities::UserLogin.documentation
  112 + failure [[401, 'Unauthorized']]
  113 + named 'ArticleFollow'
  114 + end
81 115 post ':id/follow' do
82 116 authenticate!
83 117 article = find_article(environment.articles, params[:id])
... ... @@ -92,6 +126,12 @@ module Noosfero
92 126 end
93 127 end
94 128  
  129 + desc 'Perform a vote on a article by id' do
  130 + detail 'Vote on a specific article with values: 1 (if you like) or -1 (if not)'
  131 + params Noosfero::API::Entities::UserLogin.documentation
  132 + failure [[401,'Unauthorized']]
  133 + named 'ArticleVote'
  134 + end
95 135 post ':id/vote' do
96 136 authenticate!
97 137 value = (params[:value] || 1).to_i
... ... @@ -102,13 +142,20 @@ module Noosfero
102 142 {:vote => vote.save}
103 143 end
104 144  
  145 + desc 'Return the children of a article identified by id' do
  146 + detail 'Get all children articles of a specific article'
  147 + params Noosfero::API::Entities::Article.documentation
  148 + failure [[403, 'Forbidden']]
  149 + named 'ArticleChildren'
  150 + end
105 151 get ':id/children' do
106 152 article = find_article(environment.articles, params[:id])
107 153  
108 154 #TODO make tests for this situation
109 155 votes_order = params.delete(:order) if params[:order]=='votes_score'
  156 + articles = select_filtered_collection_of(article, 'children', params)
  157 + articles = articles.display_filter(current_person, article.profile)
110 158  
111   - articles = find_articles(article, 'children')
112 159  
113 160 #TODO make tests for this situation
114 161 if votes_order
... ... @@ -119,6 +166,13 @@ module Noosfero
119 166 present articles, :with => Entities::Article, :fields => params[:fields]
120 167 end
121 168  
  169 + desc 'Return one child of a article identified by id' do
  170 + detail 'Get a child of a specific article'
  171 + params Noosfero::API::Entities::Article.documentation
  172 + success Noosfero::API::Entities::Article
  173 + failure [[403, 'Forbidden']]
  174 + named 'ArticleChild'
  175 + end
122 176 get ':id/children/:child_id' do
123 177 article = find_article(environment.articles, params[:id])
124 178 child = find_article(article.children, params[:child_id])
... ... @@ -126,6 +180,13 @@ module Noosfero
126 180 present child, :with => Entities::Article, :fields => params[:fields]
127 181 end
128 182  
  183 + desc 'Suggest a article to another profile' do
  184 + detail 'Suggest a article to another profile (person, community...)'
  185 + params Noosfero::API::Entities::Article.documentation
  186 + success Noosfero::API::Entities::Task
  187 + failure [[401,'Unauthorized']]
  188 + named 'ArticleSuggest'
  189 + end
129 190 post ':id/children/suggest' do
130 191 authenticate!
131 192 parent_article = environment.articles.find(params[:id])
... ... @@ -144,6 +205,13 @@ module Noosfero
144 205  
145 206 # Example Request:
146 207 # POST api/v1/articles/:id/children?private_token=234298743290432&article[name]=title&article[body]=body
  208 + desc 'Add a child article to a parent identified by id' do
  209 + detail 'Create a new article and associate to a parent'
  210 + params Noosfero::API::Entities::Article.documentation
  211 + success Noosfero::API::Entities::Article
  212 + failure [[401,'Unauthorized']]
  213 + named 'ArticleAddChild'
  214 + end
147 215 post ':id/children' do
148 216 authenticate!
149 217 parent_article = environment.articles.find(params[:id])
... ... @@ -173,11 +241,37 @@ module Noosfero
173 241 resource kind.pluralize.to_sym do
174 242 segment "/:#{kind}_id" do
175 243 resource :articles do
  244 +
  245 + desc "Return all articles associate with a profile of type #{kind}" do
  246 + detail 'Get a list of articles of a profile'
  247 + params Noosfero::API::Entities::Article.documentation
  248 + success Noosfero::API::Entities::Article
  249 + failure [[403, 'Forbidden']]
  250 + named 'ArticlesOfProfile'
  251 + end
176 252 get do
177 253 profile = environment.send(kind.pluralize).find(params["#{kind}_id"])
178   - present_articles(profile)
  254 +
  255 + if params[:path].present?
  256 + article = profile.articles.find_by_path(params[:path])
  257 + if !article || !article.display_to?(current_person)
  258 + article = forbidden!
  259 + end
  260 +
  261 + present article, :with => Entities::Article, :fields => params[:fields]
  262 + else
  263 +
  264 + present_articles(profile)
  265 + end
179 266 end
180 267  
  268 + desc "Return a article associate with a profile of type #{kind}" do
  269 + detail 'Get only one article of a profile'
  270 + params Noosfero::API::Entities::Article.documentation
  271 + success Noosfero::API::Entities::Article
  272 + failure [[403, 'Forbidden']]
  273 + named 'ArticleOfProfile'
  274 + end
181 275 get ':id' do
182 276 profile = environment.send(kind.pluralize).find(params["#{kind}_id"])
183 277 present_article(profile)
... ... @@ -185,6 +279,13 @@ module Noosfero
185 279  
186 280 # Example Request:
187 281 # POST api/v1/{people,communities,enterprises}/:asset_id/articles?private_token=234298743290432&article[name]=title&article[body]=body
  282 + desc "Add a new article associated with a profile of type #{kind}" do
  283 + detail 'Create a new article and associate with a profile'
  284 + params Noosfero::API::Entities::Article.documentation
  285 + success Noosfero::API::Entities::Article
  286 + failure [[403, 'Forbidden']]
  287 + named 'ArticleCreateToProfile'
  288 + end
188 289 post do
189 290 profile = environment.send(kind.pluralize).find(params["#{kind}_id"])
190 291 post_article(profile, params)
... ...
test/unit/api/articles_test.rb
... ... @@ -91,6 +91,41 @@ class ArticlesTest &lt; ActiveSupport::TestCase
91 91 assert_not_includes json['articles'].map {|a| a['id']}, child.id
92 92 end
93 93  
  94 + should 'follow a article identified by id' do
  95 + article = fast_create(Article, :profile_id => @person.id, :name => "Some thing")
  96 + post "/api/v1/articles/#{article.id}/follow?#{params.to_query}"
  97 + json = JSON.parse(last_response.body)
  98 +
  99 + assert_not_equal 401, last_response.status
  100 + assert_equal true, json['success']
  101 + end
  102 +
  103 + should 'return the followers of a article identified by id' do
  104 + article = fast_create(Article, :profile_id => @person.id, :name => "Some thing")
  105 +
  106 + article_follower = ArticleFollower.new
  107 + article_follower.article = article
  108 + article_follower.person = @person
  109 + article_follower.save!
  110 +
  111 + get "/api/v1/articles/#{article.id}/followers?"
  112 + json = JSON.parse(last_response.body)
  113 +
  114 + assert_equal 200, last_response.status
  115 + assert_equal 1, json['total_followers']
  116 + end
  117 +
  118 + should 'perform a vote in a article identified by id' do
  119 + article = fast_create(Article, :profile_id => @person.id, :name => "Some thing")
  120 + @params[:value] = 1
  121 +
  122 + post "/api/v1/articles/#{article.id}/vote?#{params.to_query}"
  123 + json = JSON.parse(last_response.body)
  124 +
  125 + assert_not_equal 401, last_response.status
  126 + assert_equal true, json['vote']
  127 + end
  128 +
94 129 expose_attributes = %w(id body abstract created_at title author profile categories image votes_for votes_against setting position hits start_date end_date tag_list parent children children_count)
95 130  
96 131 expose_attributes.each do |attr|
... ... @@ -134,6 +169,29 @@ class ArticlesTest &lt; ActiveSupport::TestCase
134 169 json = JSON.parse(last_response.body)
135 170 assert_not_includes json['articles'].map {|a| a['id']}, article.id
136 171 end
  172 +
  173 + should "return article by #{kind} and path" do
  174 + profile = fast_create(kind.camelcase.constantize, :environment_id => environment.id)
  175 + parent_article = Folder.create!(:profile => profile, :name => "Parent Folder")
  176 + article = Article.create!(:profile => profile, :name => "Some thing", :parent => parent_article)
  177 +
  178 + params[:path] = parent_article.slug+'/'+article.slug
  179 + get "/api/v1/#{kind.pluralize}/#{profile.id}/articles?#{params.to_query}"
  180 + json = JSON.parse(last_response.body)
  181 + assert_equal article.id, json["article"]["id"]
  182 + end
  183 +
  184 + should "not return article by #{kind} and path if user has no permission to view it" do
  185 + profile = fast_create(kind.camelcase.constantize, :environment_id => environment.id)
  186 + parent_article = Folder.create!(:profile => profile, :name => "Parent Folder")
  187 + article = Article.create!(:profile => profile, :name => "Some thing", :parent => parent_article, :published => false)
  188 +
  189 + assert !article.published?
  190 +
  191 + params[:path] = parent_article.slug+'/'+article.slug
  192 + get "/api/v1/#{kind.pluralize}/#{profile.id}/articles?#{params.to_query}"
  193 + assert_equal 403, last_response.status
  194 + end
137 195 end
138 196  
139 197 #############################
... ...
test/unit/api/test_helper.rb
... ... @@ -17,6 +17,11 @@ class ActiveSupport::TestCase
17 17 post "/api/v1/login?login=testapi&password=testapi"
18 18 json = JSON.parse(last_response.body)
19 19 @private_token = json["private_token"]
  20 + unless @private_token
  21 + @user.generate_private_token!
  22 + @private_token = @user.private_token
  23 + end
  24 +
20 25 @params = {:private_token => @private_token}
21 26 end
22 27 attr_accessor :private_token, :user, :person, :params, :environment
... ...