diff --git a/lib/inaturalist_api.js b/lib/inaturalist_api.js index 90e2b4ed..950f88c8 100644 --- a/lib/inaturalist_api.js +++ b/lib/inaturalist_api.js @@ -1,3 +1,4 @@ +require( "intl" ); const _ = require( "lodash" ); const jwt = require( "jsonwebtoken" ); const fs = require( "fs" ); @@ -666,8 +667,40 @@ InaturalistAPI.lookupPlaceMiddleware = ( req, res, next ) => { }; InaturalistAPI.lookupPreferredPlaceMiddleware = ( req, res, next ) => { - InaturalistAPI.lookupInstanceMiddleware( req, res, next, - "preferred_place_id", Place.findByID, "preferredPlace" ); + if ( req.query.preferred_place_id ) { + return void InaturalistAPI.lookupInstanceMiddleware( req, res, next, + "preferred_place_id", Place.findByID, "preferredPlace" ); + } + // lookup by locality code from locale + let locale = req.userSession ? req.userSession.locale : null; + if ( req.query.locale ) { + ( { locale } = req.query ); + } + if ( locale ) { + let localeObj; + try { + localeObj = new Intl.Locale( locale ); + } catch ( err ) { + // continue if locale is invalid + return void next( ); + } + const localeRegionCode = localeObj.region; + if ( !localeRegionCode ) { + return void next( ); + } + // lookup the place by code and admin-level = country + return void Place.findByLocaleCode( localeRegionCode ).then( obj => { + // no error if none of the instances exist + // locale codes are not always same as country codes + if ( _.isEmpty( obj ) ) { + return void next( ); + } + req.inat = req.inat || { }; + req.inat.preferredPlace = obj; + return void next( ); + } ).catch( next ); + } + next( ); }; InaturalistAPI.lookupUnobservedByUserMiddleware = ( req, res, next ) => { diff --git a/lib/models/place.js b/lib/models/place.js index cd2b6830..bc484100 100644 --- a/lib/models/place.js +++ b/lib/models/place.js @@ -17,6 +17,23 @@ const Place = class Place extends Model { return response.hits.hits[0] ? response.hits.hits[0]._source : null; } + static async findByLocaleCode( code ) { + const { rows } = await pgClient.connection.query( "SELECT id, name, ancestry FROM " + + "places WHERE UPPER(code) = $1 AND admin_level = 0", [code] ); + if ( !rows.length ) { + return null; + } + const result = rows[0]; + // transform ancestry field to ancestor_place_ids + let ancestorPlaceIds = null; + if ( result.ancestry ) { + ancestorPlaceIds = result.ancestry ? result.ancestry.split( "/" ).map( str => parseInt( str, 10 ) ) : []; + ancestorPlaceIds.push( result.id ); + } + const response = { id: result.id, name: result.name, ancestor_place_ids: ancestorPlaceIds }; + return response; + } + static async assignToObject( object ) { const ids = _.keys( object ); if ( ids.length === 0 ) { return; } diff --git a/package-lock.json b/package-lock.json index 5ba36340..07bd4cc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3041,6 +3041,11 @@ } } }, + "intl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/intl/-/intl-1.2.5.tgz", + "integrity": "sha1-giRKIZDE5Bn4Nx9ao02qNCDiq94=" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", diff --git a/package.json b/package.json index b940c91c..90d231e9 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "handlebars": "^4.7.6", "hapi-joi-to-swagger": "github:inaturalist/hapi-joi-to-swagger#0ec177ccc9f8306f6fbcc7066174313e593c0600", "inaturalistjs": "github:inaturalist/inaturalistjs#bfce66c2927a756d654454362bc67fc3892ba7a3", + "intl": "^1.2.5", "jsonwebtoken": "^8.3.0", "lodash": "^4.17.15", "lodash.merge": "^4.6.2", diff --git a/schema/fixtures.js b/schema/fixtures.js index e44e5076..b2ba20ce 100644 --- a/schema/fixtures.js +++ b/schema/fixtures.js @@ -1851,6 +1851,19 @@ "name": "a-place-in-a-place", "display_name": "A Place In A Place", "ancestry": "432/433" + }, + { + "id": 511, + "name": "locale-place", + "code": "LP", + "admin_level": 0, + "ancestry": "111" + }, + { + "id": 512, + "name": "locale-place-admin-level-1", + "code": "LPA", + "admin_level": 1 } ], "preferences": [ diff --git a/test/inaturalist_api.js b/test/inaturalist_api.js index c55dba82..7faca408 100644 --- a/test/inaturalist_api.js +++ b/test/inaturalist_api.js @@ -44,4 +44,38 @@ describe( "InaturalistAPI", ( ) => { } ); } ); } ); + + describe( "lookupPreferredPlaceMiddleware", ( ) => { + it( "looks up preferred_place by preferred_place_id", done => { + const req = { query: { preferred_place_id: 1, locale: "zh-LP"}, inat: {} }; + InaturalistAPI.lookupPreferredPlaceMiddleware( req, null, () => { + expect( req.inat.preferredPlace.id ).to.eq( 1 ); + done( ); + } ); + } ); + + it( "looks up preferred_place by locale", done => { + const req = { query: { locale: "zh-LP" }, inat: {} }; + InaturalistAPI.lookupPreferredPlaceMiddleware( req, null, () => { + expect( req.inat.preferredPlace.id ).to.eq( 511 ); + done( ); + } ); + } ); + + it( "does not set preferred_place if locale is malformed", done => { + const req = { query: { locale: "zh-LP-LP" }, inat: {} }; + InaturalistAPI.lookupPreferredPlaceMiddleware( req, null, () => { + expect( req.inat.preferredPlace ).to.be.undefined; + done( ); + } ); + } ); + + it( "does not set preferred_place if country is not found", done => { + const req = { query: { locale: "zh-LPB" } }; + InaturalistAPI.lookupPreferredPlaceMiddleware( req, null, () => { + expect( req.inat ).to.be.undefined; + done( ); + } ); + } ); + } ); } ); diff --git a/test/models/place.js b/test/models/place.js index 7c6458b6..995bbf2a 100644 --- a/test/models/place.js +++ b/test/models/place.js @@ -38,6 +38,27 @@ describe( "Place", ( ) => { } ); } ); + describe( "findByLocaleCode", ( ) => { + it( "returns a place given a code", async ( ) => { + const p = await Place.findByLocaleCode( "LP" ); + expect( p.id ).to.eq( 511 ); + expect( p.name ).to.eq( "locale-place" ); + expect( p.ancestor_place_ids.length ).to.eq( 2 ); + expect( p.ancestor_place_ids[0] ).to.eq( 111 ); + expect( p.ancestor_place_ids[1] ).to.eq( 511 ); + } ); + + it( "returns null if admin_level is not country", async ( ) => { + const p = await Place.findByLocaleCode( "LPA" ); + expect( p ).to.eq( null ); + } ); + + it( "returns null given an unknown code", async ( ) => { + const p = await Place.findByLocaleCode( "US" ); + expect( p ).to.eq( null ); + } ); + } ); + describe( "assignToObject", ( ) => { it( "assigns place instances to objects", done => { const o = { 1: { }, 123: { }, 432: { } };