Commit 9efdcbd7c0888f533da6deee3aafc96264732f70

Authored by Rodrigo Souto
2 parents c366e6ec 662484b4

Merge branch 'API-grape' into 'api'

Georef e cosmética

See merge request !608
app/models/profile.rb
... ... @@ -100,6 +100,48 @@ class Profile < ActiveRecord::Base
100 100 }
101 101 scope :no_templates, {:conditions => {:is_template => false}}
102 102  
  103 + # Returns a scoped object to select profiles in a given location or in a radius
  104 + # distance from the given location center.
  105 + # The parameter can be the `request.params` with the keys:
  106 + # * `country`: Country code string.
  107 + # * `state`: Second-level administrative country subdivisions.
  108 + # * `city`: City full name for center definition, or as set by users.
  109 + # * `lat`: The latitude to define the center of georef search.
  110 + # * `lng`: The longitude to define the center of georef search.
  111 + # * `distance`: Define the search radius in kilometers.
  112 + # NOTE: This method may return an exception object, to inform filter error.
  113 + # When chaining scopes, is hardly recommended you to add this as the last one,
  114 + # if you can't be sure about the provided parameters.
  115 + def self.by_location(params)
  116 + params = params.with_indifferent_access
  117 + if params[:distance].blank?
  118 + where_code = []
  119 + [ :city, :state, :country ].each do |place|
  120 + unless params[place].blank?
  121 + # ... So we must to find on this named location
  122 + # TODO: convert location attrs to a table collumn
  123 + where_code << "(profiles.data like '%#{place}: #{params[place]}%')"
  124 + end
  125 + end
  126 + self.where where_code.join(' AND ')
  127 + else # Filter in a georef circle
  128 + unless params[:lat].blank? && params[:lng].blank?
  129 + lat, lng = [ params[:lat].to_f, params[:lng].to_f ]
  130 + end
  131 + if !lat
  132 + location = [ params[:city], params[:state], params[:country] ].compact.join(', ')
  133 + if location.blank?
  134 + return Exception.new (
  135 + _('You must to provide `lat` and `lng`, or `city` and `country` to define the center of the search circle, defined by `distance`.')
  136 + )
  137 + end
  138 + lat, lng = Noosfero::GeoRef.location_to_georef location
  139 + end
  140 + dist = params[:distance].to_f
  141 + self.where "#{Noosfero::GeoRef.sql_dist lat, lng} <= #{dist}"
  142 + end
  143 + end
  144 +
103 145 include TimeScopes
104 146  
105 147 def members
... ...
lib/noosfero/api/entity.rb
1 1 class Noosfero::API::Entity < Grape::Entity
2 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[:is_inner_data]
  10 + super objects, options
  11 + else
  12 + data = super objects, options.merge(is_inner_data: true)
  13 + if objects.is_a? Exception
  14 + data.merge ok: false, error: {
  15 + type: objects.class.name,
  16 + message: objects.message
  17 + }
  18 + else
  19 + data.merge ok: true, error: { type: 'Success', message: '' }
  20 + end
  21 + end
  22 + end
  23 +
3 24 def self.fields_condition(fields)
4 25 lambda do |object, options|
5 26 return true if options[:fields].blank?
... ...
lib/noosfero/api/helpers.rb
... ... @@ -2,7 +2,7 @@ module Noosfero
2 2 module API
3 3 module APIHelpers
4 4 PRIVATE_TOKEN_PARAM = :private_token
5   - ALLOWED_PARAMETERS = [:parent_id, :from, :until, :content_type]
  5 + DEFAULT_ALLOWED_PARAMETERS = [:parent_id, :from, :until, :content_type]
6 6  
7 7 def current_user
8 8 private_token = (params[PRIVATE_TOKEN_PARAM] || headers['Private-Token']).to_s
... ... @@ -228,7 +228,7 @@ module Noosfero
228 228 def parser_params(params)
229 229 parsed_params = {}
230 230 params.map do |k,v|
231   - parsed_params[k.to_sym] = v if ALLOWED_PARAMETERS.include?(k.to_sym)
  231 + parsed_params[k.to_sym] = v if DEFAULT_ALLOWED_PARAMETERS.include?(k.to_sym)
232 232 end
233 233 parsed_params
234 234 end
... ...
lib/noosfero/api/v1/enterprises.rb
... ... @@ -5,13 +5,14 @@ module Noosfero
5 5 before { authenticate! }
6 6  
7 7 resource :enterprises do
8   -
9   - # Collect comments from articles
  8 +
  9 + # Collect enterprises from environment
10 10 #
11 11 # Parameters:
12 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 13 # oldest - Collect the oldest comments from reference_id comment. If nothing is passed the newest comments are collected
14 14 # limit - amount of comments returned. The default value is 20
  15 + # georef params - read `Profile.by_location` for more information.
15 16 #
16 17 # Example Request:
17 18 # GET /enterprises?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10
... ... @@ -19,6 +20,7 @@ module Noosfero
19 20 get do
20 21 enterprises = select_filtered_collection_of(environment, 'enterprises', params)
21 22 enterprises = enterprises.visible_for_person(current_person)
  23 + enterprises = enterprises.by_location(params) # Must be the last. May return Exception obj.
22 24 present enterprises, :with => Entities::Enterprise
23 25 end
24 26  
... ... @@ -39,7 +41,7 @@ module Noosfero
39 41 get do
40 42 person = environment.people.find(params[:person_id])
41 43 enterprises = select_filtered_collection_of(person, 'enterprises', params)
42   - enterprises = enterprises.visible
  44 + enterprises = enterprises.visible.by_location(params)
43 45 present enterprises, :with => Entities::Enterprise
44 46 end
45 47  
... ...
lib/noosfero/api/v1/people.rb
... ... @@ -4,9 +4,21 @@ module Noosfero
4 4 class People < Grape::API
5 5 before { authenticate! }
6 6  
  7 + desc 'API Root'
  8 +
7 9 resource :people do
8 10  
9   - # Collect comments from articles
  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
10 22 #
11 23 # Parameters:
12 24 # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created
... ... @@ -16,6 +28,8 @@ module Noosfero
16 28 # Example Request:
17 29 # GET /people?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10
18 30 # GET /people?reference_id=10&limit=10&oldest
  31 +
  32 + desc "Find environment's people"
19 33 get do
20 34 people = select_filtered_collection_of(environment, 'people', params)
21 35 people = people.visible_for_person(current_person)
... ...
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
... ...
test/unit/geo_ref_test.rb 0 → 100644
... ... @@ -0,0 +1,62 @@
  1 +# -*- coding: utf-8 -*-
  2 +
  3 +require File.dirname(__FILE__) + '/../test_helper'
  4 +
  5 +class GeoRefTest < ActiveSupport::TestCase
  6 +
  7 + ll = {
  8 + salvador: [-12.9, -38.5],
  9 + rio_de_janeiro: [-22.9, -43.1],
  10 + new_york: [ 40.7, -74.0],
  11 + tokyo: [ 35.6, 139.6]
  12 + }
  13 +
  14 + should 'calculate the distance between lat,lng points' do
  15 + assert_equal 1215, Noosfero::GeoRef.dist(*(ll[:salvador]+ll[:rio_de_janeiro])).round
  16 + assert_equal 6998, Noosfero::GeoRef.dist(*(ll[:salvador]+ll[:new_york])).round
  17 + assert_equal 17503, Noosfero::GeoRef.dist(*(ll[:salvador]+ll[:tokyo])).round
  18 + end
  19 +
  20 + should 'calculate the distance between a lat,lng points and a profile' do
  21 + env = fast_create Environment, name: 'SomeSite'
  22 + @acme = Enterprise.create! environment: env, identifier: 'acme', name: 'ACME',
  23 + city: 'Salvador', state: 'Bahia', country: 'BR', lat: -12.9, lng: -38.5
  24 + def sql_dist_to(ll)
  25 + ActiveRecord::Base.connection.execute(
  26 + "SELECT #{Noosfero::GeoRef.sql_dist ll[0], ll[1]} as dist" +
  27 + " FROM profiles WHERE id = #{@acme.id};"
  28 + ).first['dist'].to_f.round
  29 + end
  30 + assert_equal 1215, sql_dist_to(ll[:rio_de_janeiro])
  31 + assert_equal 6998, sql_dist_to(ll[:new_york])
  32 + assert_equal 17503, sql_dist_to(ll[:tokyo])
  33 + end
  34 +
  35 + def round_ll(ll)
  36 + ll.map{|n| n.is_a?(Float) ? n.to_i : n }
  37 + end
  38 +
  39 + should 'get lat/lng from address' do
  40 + Rails.cache.clear
  41 + ll = Noosfero::GeoRef.location_to_georef 'Salvador, Bahia, BR'
  42 + assert_equal [-12, -38, :SUCCESS], round_ll(ll)
  43 + end
  44 +
  45 + should 'get and cache lat/lng from address' do
  46 + Rails.cache.clear
  47 + ll = Noosfero::GeoRef.location_to_georef 'Curitiba, Paraná, BR'
  48 + assert_equal [-25, -49, :SUCCESS], round_ll(ll)
  49 + ll = Noosfero::GeoRef.location_to_georef 'Curitiba, Paraná, BR'
  50 + assert_equal [-25, -49, :SUCCESS, :CACHE], round_ll(ll)
  51 + end
  52 +
  53 + should 'notify a non existent address' do
  54 + Rails.cache.clear
  55 + orig_env = ENV['RAILS_ENV']
  56 + ENV['RAILS_ENV'] = 'X' # cancel throw for test mode on process_rest_req.
  57 + ll = Noosfero::GeoRef.location_to_georef 'Nowhere, Nocountry, XYZ'
  58 + ENV['RAILS_ENV'] = orig_env # restore value to do not mess with other tests.
  59 + assert_equal [0, 0, :ZERO_RESULTS], round_ll(ll)
  60 + end
  61 +
  62 +end
... ...