Commit 9efdcbd7c0888f533da6deee3aafc96264732f70
Exists in
api_tasks
and in
1 other branch
Merge branch 'API-grape' into 'api'
Georef e cosmética See merge request !608
Showing
7 changed files
with
227 additions
and
8 deletions
Show diff stats
app/models/profile.rb
| @@ -100,6 +100,48 @@ class Profile < ActiveRecord::Base | @@ -100,6 +100,48 @@ class Profile < ActiveRecord::Base | ||
| 100 | } | 100 | } |
| 101 | scope :no_templates, {:conditions => {:is_template => false}} | 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 | include TimeScopes | 145 | include TimeScopes |
| 104 | 146 | ||
| 105 | def members | 147 | def members |
lib/noosfero/api/entity.rb
| 1 | class Noosfero::API::Entity < Grape::Entity | 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 | def self.fields_condition(fields) | 24 | def self.fields_condition(fields) |
| 4 | lambda do |object, options| | 25 | lambda do |object, options| |
| 5 | return true if options[:fields].blank? | 26 | return true if options[:fields].blank? |
lib/noosfero/api/helpers.rb
| @@ -2,7 +2,7 @@ module Noosfero | @@ -2,7 +2,7 @@ module Noosfero | ||
| 2 | module API | 2 | module API |
| 3 | module APIHelpers | 3 | module APIHelpers |
| 4 | PRIVATE_TOKEN_PARAM = :private_token | 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 | def current_user | 7 | def current_user |
| 8 | private_token = (params[PRIVATE_TOKEN_PARAM] || headers['Private-Token']).to_s | 8 | private_token = (params[PRIVATE_TOKEN_PARAM] || headers['Private-Token']).to_s |
| @@ -228,7 +228,7 @@ module Noosfero | @@ -228,7 +228,7 @@ module Noosfero | ||
| 228 | def parser_params(params) | 228 | def parser_params(params) |
| 229 | parsed_params = {} | 229 | parsed_params = {} |
| 230 | params.map do |k,v| | 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 | end | 232 | end |
| 233 | parsed_params | 233 | parsed_params |
| 234 | end | 234 | end |
lib/noosfero/api/v1/enterprises.rb
| @@ -5,13 +5,14 @@ module Noosfero | @@ -5,13 +5,14 @@ module Noosfero | ||
| 5 | before { authenticate! } | 5 | before { authenticate! } |
| 6 | 6 | ||
| 7 | resource :enterprises do | 7 | resource :enterprises do |
| 8 | - | ||
| 9 | - # Collect comments from articles | 8 | + |
| 9 | + # Collect enterprises from environment | ||
| 10 | # | 10 | # |
| 11 | # Parameters: | 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 | 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 | 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 | 14 | # limit - amount of comments returned. The default value is 20 |
| 15 | + # georef params - read `Profile.by_location` for more information. | ||
| 15 | # | 16 | # |
| 16 | # Example Request: | 17 | # Example Request: |
| 17 | # GET /enterprises?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10 | 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,6 +20,7 @@ module Noosfero | ||
| 19 | get do | 20 | get do |
| 20 | enterprises = select_filtered_collection_of(environment, 'enterprises', params) | 21 | enterprises = select_filtered_collection_of(environment, 'enterprises', params) |
| 21 | enterprises = enterprises.visible_for_person(current_person) | 22 | enterprises = enterprises.visible_for_person(current_person) |
| 23 | + enterprises = enterprises.by_location(params) # Must be the last. May return Exception obj. | ||
| 22 | present enterprises, :with => Entities::Enterprise | 24 | present enterprises, :with => Entities::Enterprise |
| 23 | end | 25 | end |
| 24 | 26 | ||
| @@ -39,7 +41,7 @@ module Noosfero | @@ -39,7 +41,7 @@ module Noosfero | ||
| 39 | get do | 41 | get do |
| 40 | person = environment.people.find(params[:person_id]) | 42 | person = environment.people.find(params[:person_id]) |
| 41 | enterprises = select_filtered_collection_of(person, 'enterprises', params) | 43 | enterprises = select_filtered_collection_of(person, 'enterprises', params) |
| 42 | - enterprises = enterprises.visible | 44 | + enterprises = enterprises.visible.by_location(params) |
| 43 | present enterprises, :with => Entities::Enterprise | 45 | present enterprises, :with => Entities::Enterprise |
| 44 | end | 46 | end |
| 45 | 47 |
lib/noosfero/api/v1/people.rb
| @@ -4,9 +4,21 @@ module Noosfero | @@ -4,9 +4,21 @@ module Noosfero | ||
| 4 | class People < Grape::API | 4 | class People < Grape::API |
| 5 | before { authenticate! } | 5 | before { authenticate! } |
| 6 | 6 | ||
| 7 | + desc 'API Root' | ||
| 8 | + | ||
| 7 | resource :people do | 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 | # Parameters: | 23 | # 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 | 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,6 +28,8 @@ module Noosfero | ||
| 16 | # Example Request: | 28 | # Example Request: |
| 17 | # GET /people?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10 | 29 | # GET /people?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10 |
| 18 | # GET /people?reference_id=10&limit=10&oldest | 30 | # GET /people?reference_id=10&limit=10&oldest |
| 31 | + | ||
| 32 | + desc "Find environment's people" | ||
| 19 | get do | 33 | get do |
| 20 | people = select_filtered_collection_of(environment, 'people', params) | 34 | people = select_filtered_collection_of(environment, 'people', params) |
| 21 | people = people.visible_for_person(current_person) | 35 | people = people.visible_for_person(current_person) |
lib/noosfero/geo_ref.rb
| 1 | module Noosfero::GeoRef | 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 | end | 84 | end |
| @@ -0,0 +1,62 @@ | @@ -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 |