Commit 043bbbd13b70409a9cb6d837a54144ed661bd28e

Authored by Victor Costa
2 parents 4b06d2fb 83a8cdbf

Merge branch 'noosfero_grape_api' into rails3_api

Conflicts:
	app/models/article.rb
	app/models/user.rb
	config.ru
app/models/article.rb
... ... @@ -102,6 +102,16 @@ class Article < ActiveRecord::Base
102 102 {:include => 'categories_including_virtual', :conditions => { 'categories.id' => category.id }}
103 103 }
104 104  
  105 + #FIXME make this test
  106 + scope :newer_than, lambda { |reference_id|
  107 + {:conditions => ["articles.id > #{reference_id}"]}
  108 + }
  109 +
  110 + #FIXME make this test
  111 + scope :older_than, lambda { |reference_id|
  112 + {:conditions => ["articles.id < #{reference_id}"]}
  113 + }
  114 +
105 115 scope :by_range, lambda { |range| {
106 116 :conditions => [
107 117 'published_at BETWEEN :start_date AND :end_date', { :start_date => range.first, :end_date => range.last }
... ... @@ -658,6 +668,11 @@ class Article &lt; ActiveRecord::Base
658 668 person ? person.id : nil
659 669 end
660 670  
  671 + #FIXME make this test
  672 + def author_custom_image(size = :icon)
  673 + author ? author.profile_custom_image(size) : nil
  674 + end
  675 +
661 676 def version_license(version_number = nil)
662 677 return license if version_number.nil?
663 678 profile.environment.licenses.find_by_id(versions.find_by_version(version_number).license_id)
... ...
app/models/comment.rb
... ... @@ -20,6 +20,17 @@ class Comment &lt; ActiveRecord::Base
20 20  
21 21 scope :without_reply, :conditions => ['reply_of_id IS NULL']
22 22  
  23 + #FIXME make this test
  24 + named_scope :newer_than, lambda { |reference_id|
  25 + {:conditions => ["comments.id > #{reference_id}"]}
  26 + }
  27 +
  28 + #FIXME make this test
  29 + named_scope :older_than, lambda { |reference_id|
  30 + {:conditions => ["comments.id < #{reference_id}"]}
  31 + }
  32 +
  33 +
23 34 # unauthenticated authors:
24 35 validates_presence_of :name, :if => (lambda { |record| !record.email.blank? })
25 36 validates_presence_of :email, :if => (lambda { |record| !record.name.blank? })
... ... @@ -63,6 +74,11 @@ class Comment &lt; ActiveRecord::Base
63 74 author ? author.url : nil
64 75 end
65 76  
  77 + #FIXME make this test
  78 + def author_custom_image(size = :icon)
  79 + author ? author.profile_custom_image(size) : nil
  80 + end
  81 +
66 82 def url
67 83 article.view_url.merge(:anchor => anchor)
68 84 end
... ...
app/models/profile.rb
... ... @@ -877,6 +877,13 @@ private :generate_url, :url_options
877 877 image.public_filename(:icon) if image.present?
878 878 end
879 879  
  880 + #FIXME make this test
  881 + def profile_custom_image(size = :icon)
  882 + image_path = profile_custom_icon if size == :icon
  883 + image_path ||= image.public_filename(size) if image.present?
  884 + image_path
  885 + end
  886 +
880 887 def jid(options = {})
881 888 domain = options[:domain] || environment.default_hostname
882 889 "#{identifier}@#{domain}"
... ...
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.
... ... @@ -138,6 +139,18 @@ class User &lt; ActiveRecord::Base
138 139 u && u.authenticated?(password) ? u : nil
139 140 end
140 141  
  142 + #FIXME make this test
  143 + def generate_private_token!
  144 + self.private_token = SecureRandom.hex
  145 + self.private_token_generated_at = DateTime.now
  146 + save(false)
  147 + end
  148 +
  149 + #FIXME make this test
  150 + def private_token_expired?
  151 + self.generate_private_token! if self.private_token.nil? || (self.private_token_generated_at + 2.weeks < DateTime.now)
  152 + end
  153 +
141 154 # Activates the user in the database.
142 155 def activate
143 156 return false unless self.person
... ...
config.ru
... ... @@ -2,3 +2,14 @@
2 2  
3 3 require ::File.expand_path('../config/environment', __FILE__)
4 4 run Noosfero::Application
  5 +
  6 +rails_app = Rack::Builder.new do
  7 + use Rails::Rack::LogTailer
  8 + use Rails::Rack::Static
  9 + run ActionController::Dispatcher.new
  10 +end
  11 +
  12 +run Rack::Cascade.new([
  13 + API::API,
  14 + rails_app
  15 +])
5 16 \ No newline at end of file
... ...
db/migrate/20140407013817_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
... ...
lib/api/api.rb 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +require 'grape'
  2 +Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file}
  3 +
  4 +module API
  5 + class API < Grape::API
  6 + version 'v1'
  7 + prefix "api"
  8 + format :json
  9 + content_type :txt, "text/plain"
  10 +
  11 + helpers APIHelpers
  12 +
  13 + mount V1::Articles
  14 + mount V1::Comments
  15 + mount V1::Users
  16 + mount Session
  17 +
  18 + end
  19 +end
... ...
lib/api/entities.rb 0 → 100644
... ... @@ -0,0 +1,59 @@
  1 +module API
  2 + module Entities
  3 + class Article < Grape::Entity
  4 + root 'articles', 'article'
  5 + expose :id, :body, :created_at
  6 + expose :title, :documentation => {:type => "String", :desc => "Title of the article"}
  7 +
  8 + expose :author do |article, options|
  9 + {
  10 + :id => article.author_id,
  11 + :name => article.author_name,
  12 + :icon_url => article.author_custom_image(:icon),
  13 + :minor_url => article.author_custom_image(:minor),
  14 + :portrait_url => article.author_custom_image(:portrait),
  15 + :thumb_url => article.author_custom_image(:thumb),
  16 + }
  17 + end
  18 +
  19 + expose :profile do |article, options|
  20 + {
  21 + :id => article.profile.id,
  22 + :name => article.profile.name,
  23 + :icon_url => article.profile.profile_custom_image(:icon),
  24 + :minor_url => article.profile.profile_custom_image(:minor),
  25 + :portrait_url => article.profile.profile_custom_image(:portrait),
  26 + :thumb_url => article.profile.profile_custom_image(:thumb),
  27 + }
  28 + end
  29 +
  30 + end
  31 +
  32 + class Comment < Grape::Entity
  33 + root 'comments', 'comment'
  34 + expose :body, :title, :created_at, :id
  35 +
  36 + expose :author do |comment, options|
  37 + {
  38 + :id => comment.author_id,
  39 + :name => comment.author_name,
  40 + :icon_url => comment.author_custom_image(:icon),
  41 + :minor_url => comment.author_custom_image(:minor),
  42 + :portrait_url => comment.author_custom_image(:portrait),
  43 + :thumb_url => comment.author_custom_image(:thumb),
  44 + }
  45 + end
  46 +
  47 + end
  48 +
  49 + class User < Grape::Entity
  50 + root 'users', 'user'
  51 + expose :login
  52 + end
  53 +
  54 + class UserLogin < User
  55 + expose :private_token
  56 + end
  57 +
  58 + end
  59 +end
... ...
lib/api/helpers.rb 0 → 100644
... ... @@ -0,0 +1,181 @@
  1 +module API
  2 + module APIHelpers
  3 + PRIVATE_TOKEN_PARAM = :private_token
  4 +
  5 + def logger
  6 + API.logger
  7 + end
  8 +
  9 + def current_user
  10 + private_token = params[PRIVATE_TOKEN_PARAM].to_s
  11 + @current_user ||= User.find_by_private_token(private_token)
  12 + @current_user = nil if !@current_user.nil? && @current_user.private_token_expired?
  13 + @current_user
  14 + end
  15 +
  16 + def current_person
  17 + current_user.person unless current_user.nil?
  18 + end
  19 +
  20 + def logout
  21 + @current_user = nil
  22 + end
  23 +
  24 + def environment
  25 + @environment
  26 + end
  27 +
  28 + def limit
  29 + limit = params[:limit].to_i
  30 + limit = default_limit if limit <= 0
  31 + limit
  32 + end
  33 +
  34 + def period(from_date, until_date)
  35 + if from_date.nil?
  36 + begin_period = Time.at(0).to_datetime
  37 + end_period = until_date.nil? ? DateTime.now : until_date
  38 + else
  39 + begin_period = from_date
  40 + end_period = DateTime.now
  41 + end
  42 +
  43 + begin_period...end_period
  44 + end
  45 +
  46 + def parse_content_type(content_type)
  47 + return nil if content_type.blank?
  48 + content_type.split(',').map do |content_type|
  49 + content_type.camelcase
  50 + end
  51 + end
  52 +
  53 +
  54 + def make_conditions_with_parameter(params = {})
  55 + conditions = {}
  56 + from_date = DateTime.parse(params[:from]) if params[:from]
  57 + until_date = DateTime.parse(params[:until]) if params[:until]
  58 +
  59 + conditions[:type] = parse_content_type(params[:content_type]) unless params[:content_type].nil?
  60 +
  61 + conditions[:created_at] = period(from_date, until_date)
  62 +
  63 + conditions
  64 + end
  65 +
  66 +#FIXME see if its needed
  67 +# def paginate(relation)
  68 +# per_page = params[:per_page].to_i
  69 +# paginated = relation.page(params[:page]).per(per_page)
  70 +# add_pagination_headers(paginated, per_page)
  71 +#
  72 +# paginated
  73 +# end
  74 +
  75 + def authenticate!
  76 + unauthorized! unless current_user
  77 + end
  78 +
  79 +#FIXME see if its needed
  80 +# def authenticated_as_admin!
  81 +# forbidden! unless current_user.is_admin?
  82 +# end
  83 +#
  84 +#FIXME see if its needed
  85 +# def authorize! action, subject
  86 +# unless abilities.allowed?(current_user, action, subject)
  87 +# forbidden!
  88 +# end
  89 +# end
  90 +#
  91 +#FIXME see if its needed
  92 +# def can?(object, action, subject)
  93 +# abilities.allowed?(object, action, subject)
  94 +# end
  95 +
  96 + # Checks the occurrences of required attributes, each attribute must be present in the params hash
  97 + # or a Bad Request error is invoked.
  98 + #
  99 + # Parameters:
  100 + # keys (required) - A hash consisting of keys that must be present
  101 + def required_attributes!(keys)
  102 + keys.each do |key|
  103 + bad_request!(key) unless params[key].present?
  104 + end
  105 + end
  106 +
  107 + # Checks the occurrences of uniqueness of attributes, each attribute must be present in the params hash
  108 + # or a Bad Request error is invoked.
  109 + #
  110 + # Parameters:
  111 + # keys (unique) - A hash consisting of keys that must be unique
  112 + def unique_attributes!(obj, keys)
  113 + keys.each do |key|
  114 + cant_be_saved_request!(key) if obj.send("find_by_#{key.to_s}", params[key])
  115 + end
  116 + end
  117 +
  118 + def attributes_for_keys(keys)
  119 + attrs = {}
  120 + keys.each do |key|
  121 + attrs[key] = params[key] if params[key].present? or (params.has_key?(key) and params[key] == false)
  122 + end
  123 + attrs
  124 + end
  125 +
  126 + # error helpers
  127 + def forbidden!
  128 + render_api_error!('403 Forbidden', 403)
  129 + end
  130 +
  131 + def cant_be_saved_request!(attribute)
  132 + message = _("(Invalid request) #{attribute} can't be saved")
  133 + render_api_error!(message, 400)
  134 + end
  135 +
  136 + def bad_request!(attribute)
  137 + message = _("(Bad request) #{attribute} not given")
  138 + render_api_error!(message, 400)
  139 + end
  140 +
  141 + def something_wrong!
  142 + message = _("Something wrong happened")
  143 + render_api_error!(message, 400)
  144 + end
  145 +
  146 + def unauthorized!
  147 + render_api_error!(_('Unauthorized'), 401)
  148 + end
  149 +
  150 + def not_allowed!
  151 + render_api_error!(_('Method Not Allowed'), 405)
  152 + end
  153 +
  154 + def render_api_error!(message, status)
  155 + error!({'message' => message, :code => status}, status)
  156 + end
  157 +
  158 + protected
  159 +
  160 + def detect_stuff_by_domain
  161 + @domain = Domain.find_by_name(request.host)
  162 + if @domain.nil?
  163 + @environment = Environment.default
  164 + if @environment.nil? && Rails.env.development?
  165 + # This should only happen in development ...
  166 + @environment = Environment.create!(:name => "Noosfero", :is_default => true)
  167 + end
  168 + else
  169 + @environment = @domain.environment
  170 + end
  171 + end
  172 +
  173 + private
  174 +
  175 + def default_limit
  176 + 20
  177 + end
  178 +
  179 +
  180 + end
  181 +end
... ...
lib/api/session.rb 0 → 100644
... ... @@ -0,0 +1,44 @@
  1 +module API
  2 +
  3 + class Session < Grape::API
  4 +
  5 + # Login to get token
  6 + #
  7 + # Parameters:
  8 + # login (*required) - user login or email
  9 + # password (required) - user password
  10 + #
  11 + # Example Request:
  12 + # POST /login?login=some&password=pass
  13 + post "/login" do
  14 + user ||= User.authenticate(params[:login], params[:password], environment)
  15 +
  16 + return unauthorized! unless user
  17 + user.generate_private_token!
  18 + present user, :with => Entities::UserLogin
  19 + end
  20 +
  21 + # Create user.
  22 + #
  23 + # Parameters:
  24 + # email (required) - Email
  25 + # password (required) - Password
  26 + # login - login
  27 + # Example Request:
  28 + # POST /register?email=some@mail.com&password=pas&login=some
  29 + post "/register" do
  30 + required_attributes! [:email, :login, :password]
  31 + unique_attributes! User, [:email, :login]
  32 + attrs = attributes_for_keys [:email, :login, :password]
  33 + attrs[:password_confirmation] = attrs[:password]
  34 + user = User.new(attrs)
  35 + if user.save
  36 + user.activate
  37 + present user, :with => Entities::User
  38 + else
  39 + something_wrong!
  40 + end
  41 + end
  42 +
  43 + end
  44 +end
... ...
lib/api/v1/articles.rb 0 → 100644
... ... @@ -0,0 +1,60 @@
  1 +module API
  2 + module V1
  3 + class Articles < Grape::API
  4 + before { detect_stuff_by_domain }
  5 + before { authenticate! }
  6 +
  7 + resource :articles 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 /articles?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10&content_type=Hub
  18 +# desc 'Articles.', {
  19 +# :params => API::Entities::Article.documentation
  20 +# }
  21 + get do
  22 +
  23 + conditions = make_conditions_with_parameter(params)
  24 +
  25 + if params[:reference_id]
  26 + articles = environment.articles.send("#{params.key?(:oldest) ? 'older_than' : 'newer_than'}", params[:reference_id]).find(:all, :conditions => conditions, :limit => limit, :order => "created_at DESC")
  27 + else
  28 + articles = environment.articles.find(:all, :conditions => conditions, :limit => limit, :order => "created_at DESC")
  29 + end
  30 + present articles, :with => Entities::Article
  31 + end
  32 +
  33 + desc "Return the article id"
  34 + get ':id' do
  35 + present environment.articles.find(params[:id]), :with => Entities::Article
  36 + end
  37 +
  38 + get ':id/children' do
  39 + from_date = DateTime.parse(params[:from]) if params[:from]
  40 + until_date = DateTime.parse(params[:until]) if params[:until]
  41 +
  42 + conditions = make_conditions_with_parameter(params)
  43 + if params[:reference_id]
  44 + articles = environment.articles.find(params[:id]).children.send("#{params.key?(:oldest) ? 'older_than' : 'newer_than'}", params[:reference_id]).find(:all, :conditions => conditions, :limit => limit, :order => "created_at DESC")
  45 + else
  46 + articles = environment.articles.find(params[:id]).children.find(:all, :conditions => conditions, :limit => limit, :order => "created_at DESC")
  47 + end
  48 + present articles, :with => Entities::Article
  49 + end
  50 +
  51 + get ':id/children/:child_id' do
  52 + present environment.articles.find(params[:id]).children.find(params[:child_id]), :with => Entities::Article
  53 + end
  54 +
  55 +
  56 + end
  57 +
  58 + end
  59 + end
  60 +end
... ...
lib/api/v1/comments.rb 0 → 100644
... ... @@ -0,0 +1,44 @@
  1 +module API
  2 + module V1
  3 + class Comments < Grape::API
  4 +
  5 + before { detect_stuff_by_domain }
  6 + before { authenticate! }
  7 +
  8 + resource :articles do
  9 + # Collect comments from articles
  10 + #
  11 + # Parameters:
  12 + # reference_id - comment id used as reference to collect comment
  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 /articles/12/comments?oldest&limit=10&reference_id=23
  18 + get ":id/comments" do
  19 +
  20 + conditions = make_conditions_with_parameter(params)
  21 +
  22 + if params[:reference_id]
  23 + comments = environment.articles.find(params[:id]).comments.send("#{params.key?(:oldest) ? 'older_than' : 'newer_than'}", params[:reference_id]).find(:all, :conditions => conditions, :limit => limit, :order => "created_at DESC")
  24 + else
  25 + comments = environment.articles.find(params[:id]).comments.find(:all, :conditions => conditions, :limit => limit, :order => "created_at DESC")
  26 + end
  27 + present comments, :with => Entities::Comment
  28 +
  29 + end
  30 +
  31 + get ":id/comments/:comment_id" do
  32 + present environment.articles.find(params[:id]).comments.find(params[:comment_id]), :with => Entities::Comment
  33 + end
  34 +
  35 + # Example Request:
  36 + # POST api/v1/articles/12/comments?private_toke=234298743290432&body=new comment
  37 + post ":id/comments" do
  38 + present environment.articles.find(params[:id]).comments.create(:author => current_person, :body => params[:body]), :with => Entities::Comment
  39 + end
  40 + end
  41 +
  42 + end
  43 + end
  44 +end
... ...
lib/api/v1/users.rb 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +module API
  2 + module V1
  3 + class Users < Grape::API
  4 +
  5 + before { authenticate! }
  6 +
  7 + resource :users do
  8 +
  9 + #FIXME make the pagination
  10 + #FIXME put it on environment context
  11 + get do
  12 + present User.all, :with => Entities::User
  13 + end
  14 +
  15 + get ":id" do
  16 + present User.find(params[:id]), :with => Entities::User
  17 + end
  18 +
  19 + end
  20 +
  21 + end
  22 + end
  23 +end
... ...