[Git][noosfero/noosfero][api] 8 commits: Ads GeoRef lib

Rodrigo Souto gitlab at gitlab.com
Tue Jun 23 10:43:32 BRT 2015


Rodrigo Souto pushed to branch api at Noosfero / noosfero


Commits:
bacb490a by Aurélio A. Heckert at 2015-06-19T14:17:41Z
Ads GeoRef lib

- - - - -
6c54d1c3 by Aurélio A. Heckert at 2015-06-19T14:17:42Z
Filter profile by location

- - - - -
f0260209 by Aurélio A. Heckert at 2015-06-19T14:19:25Z
API Filter enterprises by location

- - - - -
6b50511e by Aurélio A. Heckert at 2015-06-19T14:19:25Z
API response with error block

- - - - -
8bc5fc88 by Aurélio A. Heckert at 2015-06-19T14:20:29Z
API cant filter People by location

A note about privacy, prevent devs to commit a mistake.

- - - - -
2077d2b6 by Aurélio A. Heckert at 2015-06-19T14:23:27Z
Cosmetic changes on API code

- - - - -
662484b4 by Aurélio A. Heckert at 2015-06-19T14:23:27Z
add a description to People API

- - - - -
9efdcbd7 by Rodrigo Souto at 2015-06-23T13:43:25Z
Merge branch 'API-grape' into 'api'

Georef e cosmética

See merge request !608

- - - - -


7 changed files:

- app/models/profile.rb
- lib/noosfero/api/entity.rb
- lib/noosfero/api/helpers.rb
- lib/noosfero/api/v1/enterprises.rb
- lib/noosfero/api/v1/people.rb
- lib/noosfero/geo_ref.rb
- + test/unit/geo_ref_test.rb


Changes:

=====================================
app/models/profile.rb
=====================================
--- a/app/models/profile.rb
+++ b/app/models/profile.rb
@@ -100,6 +100,48 @@ class Profile < ActiveRecord::Base
   }
   scope :no_templates, {:conditions => {:is_template => false}}
 
+  # Returns a scoped object to select profiles in a given location or in a radius
+  # distance from the given location center.
+  # The parameter can be the `request.params` with the keys:
+  # * `country`: Country code string.
+  # * `state`: Second-level administrative country subdivisions.
+  # * `city`: City full name for center definition, or as set by users.
+  # * `lat`: The latitude to define the center of georef search.
+  # * `lng`: The longitude to define the center of georef search.
+  # * `distance`: Define the search radius in kilometers.
+  # NOTE: This method may return an exception object, to inform filter error.
+  # When chaining scopes, is hardly recommended you to add this as the last one,
+  # if you can't be sure about the provided parameters.
+  def self.by_location(params)
+    params = params.with_indifferent_access
+    if params[:distance].blank?
+      where_code = []
+      [ :city, :state, :country ].each do |place|
+        unless params[place].blank?
+          # ... So we must to find on this named location
+          # TODO: convert location attrs to a table collumn
+          where_code << "(profiles.data like '%#{place}: #{params[place]}%')"
+        end
+      end
+      self.where where_code.join(' AND ')
+    else # Filter in a georef circle
+      unless params[:lat].blank? && params[:lng].blank?
+        lat, lng = [ params[:lat].to_f, params[:lng].to_f ]
+      end
+      if !lat
+        location = [ params[:city], params[:state], params[:country] ].compact.join(', ')
+        if location.blank?
+          return Exception.new (
+            _('You must to provide `lat` and `lng`, or `city` and `country` to define the center of the search circle, defined by `distance`.')
+          )
+        end
+        lat, lng = Noosfero::GeoRef.location_to_georef location
+      end
+      dist = params[:distance].to_f
+      self.where "#{Noosfero::GeoRef.sql_dist lat, lng} <= #{dist}"
+    end
+  end
+
   include TimeScopes
 
   def members


=====================================
lib/noosfero/api/entity.rb
=====================================
--- a/lib/noosfero/api/entity.rb
+++ b/lib/noosfero/api/entity.rb
@@ -1,5 +1,26 @@
 class Noosfero::API::Entity < Grape::Entity
 
+  def initialize(object, options = {})
+    object = nil if object.is_a? Exception
+    super object, options
+  end
+
+  def self.represent(objects, options = {})
+    if options[:is_inner_data]
+      super objects, options
+    else
+      data = super objects, options.merge(is_inner_data: true)
+      if objects.is_a? Exception
+        data.merge ok: false, error: {
+          type: objects.class.name,
+          message: objects.message
+        }
+      else
+        data.merge ok: true, error: { type: 'Success', message: '' }
+      end
+    end
+  end
+
   def self.fields_condition(fields)
     lambda do |object, options|
       return true if options[:fields].blank?


=====================================
lib/noosfero/api/helpers.rb
=====================================
--- a/lib/noosfero/api/helpers.rb
+++ b/lib/noosfero/api/helpers.rb
@@ -2,7 +2,7 @@ module Noosfero
   module API
     module APIHelpers
       PRIVATE_TOKEN_PARAM = :private_token
-      ALLOWED_PARAMETERS = [:parent_id, :from, :until, :content_type]
+      DEFAULT_ALLOWED_PARAMETERS = [:parent_id, :from, :until, :content_type]
 
       def current_user
         private_token = (params[PRIVATE_TOKEN_PARAM] || headers['Private-Token']).to_s
@@ -228,7 +228,7 @@ module Noosfero
       def parser_params(params)
         parsed_params = {}
         params.map do |k,v|
-          parsed_params[k.to_sym] = v if ALLOWED_PARAMETERS.include?(k.to_sym)
+          parsed_params[k.to_sym] = v if DEFAULT_ALLOWED_PARAMETERS.include?(k.to_sym)
         end
         parsed_params
       end


=====================================
lib/noosfero/api/v1/enterprises.rb
=====================================
--- a/lib/noosfero/api/v1/enterprises.rb
+++ b/lib/noosfero/api/v1/enterprises.rb
@@ -5,13 +5,14 @@ module Noosfero
         before { authenticate! }
 
         resource :enterprises do
-
-          # Collect comments from articles
+  
+          # Collect enterprises from environment
           #
           # Parameters:
           #   from             - date where the search will begin. If nothing is passed the default date will be the date of the first article created
           #   oldest           - Collect the oldest comments from reference_id comment. If nothing is passed the newest comments are collected
           #   limit            - amount of comments returned. The default value is 20
+          #   georef params    - read `Profile.by_location` for more information.
           #
           # Example Request:
           #  GET /enterprises?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10
@@ -19,6 +20,7 @@ module Noosfero
           get do
             enterprises = select_filtered_collection_of(environment, 'enterprises', params)
             enterprises = enterprises.visible_for_person(current_person)
+            enterprises = enterprises.by_location(params) # Must be the last. May return Exception obj.
             present enterprises, :with => Entities::Enterprise
           end
 
@@ -39,7 +41,7 @@ module Noosfero
               get do
                 person = environment.people.find(params[:person_id])
                 enterprises = select_filtered_collection_of(person, 'enterprises', params)
-                enterprises = enterprises.visible
+                enterprises = enterprises.visible.by_location(params)
                 present enterprises, :with => Entities::Enterprise
               end
 


=====================================
lib/noosfero/api/v1/people.rb
=====================================
--- a/lib/noosfero/api/v1/people.rb
+++ b/lib/noosfero/api/v1/people.rb
@@ -4,9 +4,21 @@ module Noosfero
       class People < Grape::API
         before { authenticate! }
 
+        desc 'API Root'
+
         resource :people do
 
-          # Collect comments from articles
+          # -- A note about privacy --
+          # We wold find people by location, but we must test if the related
+          # fields are public. We can't do it now, with SQL, while the location
+          # data and the fields_privacy are a serialized settings.
+          # We must build a new table for profile data, where we can set meta-data
+          # like:
+          # | id | profile_id | key  | value    | privacy_level | source    |
+          # |  1 |         99 | city | Salvador | friends       | user      |
+          # |  2 |         99 | lng  |  -38.521 | me only       | automatic |
+
+          # Collect people from environment
           #
           # Parameters:
           #   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
           # Example Request:
           #  GET /people?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10
           #  GET /people?reference_id=10&limit=10&oldest
+
+          desc "Find environment's people"
           get do
             people = select_filtered_collection_of(environment, 'people', params)
             people = people.visible_for_person(current_person)


=====================================
lib/noosfero/geo_ref.rb
=====================================
--- a/lib/noosfero/geo_ref.rb
+++ b/lib/noosfero/geo_ref.rb
@@ -1,6 +1,84 @@
 module Noosfero::GeoRef
 
-  KM_LAT = 111.2 # aproximate distance in km for 1 degree latitude
-  KM_LNG = 85.3 # aproximate distance in km for 1 degree longitude
+  # May replace this module by http://www.postgresql.org/docs/9.3/static/earthdistance.html
+
+  EARTH_RADIUS = 6378 # aproximate in km
+
+  class << self
+
+    def dist(lat1, lng1, lat2, lng2)
+      def deg2rad(d); (d*Math::PI)/180; end
+      def c(n); Math.cos(n); end
+      def s(n); Math.sin(n); end
+      lat1 = deg2rad lat1
+      lat2 = deg2rad lat2
+      dlng = deg2rad(lng2) - deg2rad(lng1)
+      EARTH_RADIUS * Math.atan2(
+        Math.sqrt(
+          ( c(lat2) * s(dlng) )**2 +
+          ( c(lat1) * s(lat2) - s(lat1) * c(lat2) * c(dlng) )**2
+        ),
+        s(lat1) * s(lat2) + c(lat1) * c(lat2) * c(dlng)
+      )
+    end
+
+    # Write a SQL expression to return the distance from a profile to a
+    # reference point, in kilometers.
+    # http://www.plumislandmedia.net/mysql/vicenty-great-circle-distance-formula
+    def sql_dist(ref_lat, ref_lng)
+      "2*PI()*#{EARTH_RADIUS}*(
+        DEGREES(
+          ATAN2(
+            SQRT(
+              POW(COS(RADIANS(#{ref_lat}))*SIN(RADIANS(#{ref_lng}-lng)),2) +
+              POW(
+                COS(RADIANS(lat)) * SIN(RADIANS(#{ref_lat})) - (
+                  SIN(RADIANS(lat)) * COS(RADIANS(#{ref_lat})) * COS(RADIANS(#{ref_lng}-lng))
+                ), 2
+              )
+            ),
+            SIN(RADIANS(lat)) * SIN(RADIANS(#{ref_lat})) +
+            COS(RADIANS(lat)) * COS(RADIANS(#{ref_lat})) * COS(RADIANS(#{ref_lng}-lng))
+          )
+        )/360
+      )"
+    end
+
+    # Asks Google for the georef of a location.
+    def location_to_georef(location)
+      key = location.downcase
+      ll = Rails.cache.read key
+      return ll + [:CACHE] if ll.kind_of? Array
+      resp = RestClient.get 'https://maps.googleapis.com/maps/api/geocode/json?' +
+                            'sensor=false&address=' + url_encode(location)
+      if resp.nil? || resp.code.to_i != 200
+        if ENV['RAILS_ENV'] == 'test'
+          print " Google Maps API fail (code #{resp ? resp.code : :nil}) "
+        else
+          Rails.logger.warn "Google Maps API request information for " +
+                            "\"#{location}\" fail. (code #{resp ? resp.code : :nil})"
+        end
+        return [ 0, 0, "HTTP_FAIL_#{resp.code}".to_sym ] # do not cache failed response
+      else
+        json = JSON.parse resp.body
+        if json && (r=json['results']) && (r=r[0]) && (r=r['geometry']) &&
+           (r=r['location']) && r['lat']
+          ll = [ r['lat'], r['lng'], :SUCCESS ]
+        else
+          status = json['status'] || 'Undefined Error'
+          message = "Google Maps API cant find \"#{location}\" (#{status})"
+          if ENV['RAILS_ENV'] == 'test'
+            print " #{message} "
+          else
+            Rails.logger.warn message
+          end
+          ll = [ 0, 0, status.to_sym ]
+        end
+        Rails.cache.write key, ll
+      end
+      ll
+    end
+
+  end
 
 end


=====================================
test/unit/geo_ref_test.rb
=====================================
--- /dev/null
+++ b/test/unit/geo_ref_test.rb
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class GeoRefTest < ActiveSupport::TestCase
+
+  ll = {
+    salvador:       [-12.9, -38.5],
+    rio_de_janeiro: [-22.9, -43.1],
+    new_york:       [ 40.7, -74.0],
+    tokyo:          [ 35.6, 139.6]
+  }
+
+  should 'calculate the distance between lat,lng points' do
+    assert_equal 1215, Noosfero::GeoRef.dist(*(ll[:salvador]+ll[:rio_de_janeiro])).round
+    assert_equal 6998, Noosfero::GeoRef.dist(*(ll[:salvador]+ll[:new_york])).round
+    assert_equal 17503, Noosfero::GeoRef.dist(*(ll[:salvador]+ll[:tokyo])).round
+  end
+
+  should 'calculate the distance between a lat,lng points and a profile' do
+    env = fast_create Environment, name: 'SomeSite'
+    @acme = Enterprise.create! environment: env, identifier: 'acme', name: 'ACME',
+        city: 'Salvador', state: 'Bahia', country: 'BR', lat: -12.9, lng: -38.5
+    def sql_dist_to(ll)
+      ActiveRecord::Base.connection.execute(
+        "SELECT #{Noosfero::GeoRef.sql_dist ll[0], ll[1]} as dist" +
+        " FROM profiles WHERE id = #{@acme.id};"
+      ).first['dist'].to_f.round
+    end
+    assert_equal 1215, sql_dist_to(ll[:rio_de_janeiro])
+    assert_equal 6998, sql_dist_to(ll[:new_york])
+    assert_equal 17503, sql_dist_to(ll[:tokyo])
+  end
+
+  def round_ll(ll)
+    ll.map{|n| n.is_a?(Float) ? n.to_i : n }
+  end
+
+  should 'get lat/lng from address' do
+    Rails.cache.clear
+    ll = Noosfero::GeoRef.location_to_georef 'Salvador, Bahia, BR'
+    assert_equal [-12, -38, :SUCCESS], round_ll(ll)
+  end
+
+  should 'get and cache lat/lng from address' do
+    Rails.cache.clear
+    ll = Noosfero::GeoRef.location_to_georef 'Curitiba, Paraná, BR'
+    assert_equal [-25, -49, :SUCCESS], round_ll(ll)
+    ll = Noosfero::GeoRef.location_to_georef 'Curitiba, Paraná, BR'
+    assert_equal [-25, -49, :SUCCESS, :CACHE], round_ll(ll)
+  end
+
+  should 'notify a non existent address' do
+    Rails.cache.clear
+    orig_env = ENV['RAILS_ENV']
+    ENV['RAILS_ENV'] = 'X' # cancel throw for test mode on process_rest_req.
+    ll = Noosfero::GeoRef.location_to_georef 'Nowhere, Nocountry, XYZ'
+    ENV['RAILS_ENV'] = orig_env # restore value to do not mess with other tests.
+    assert_equal [0, 0, :ZERO_RESULTS], round_ll(ll)
+  end
+
+end



View it on GitLab: https://gitlab.com/noosfero/noosfero/compare/c366e6ecb18b9aa7add7bc3f2a3a93051babef28...9efdcbd7c0888f533da6deee3aafc96264732f70
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://listas.softwarelivre.org/pipermail/noosfero-dev/attachments/20150623/94920701/attachment-0001.html>


More information about the Noosfero-dev mailing list