From 22e36be4f23eb662fd2d3ffa8c70be433c47516b Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 4 Dec 2018 23:13:42 -0500 Subject: [PATCH 1/5] WIP on replacing documentation with direct calls to OSM wikibase --- modules/services/index.js | 3 ++ modules/services/osm_wikibase.js | 74 ++++++++++++++++++++++++++++++ modules/ui/tag_reference.js | 56 +++++++++++----------- test/spec/services/osm_wikibase.js | 49 ++++++++++++++++++++ 4 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 modules/services/osm_wikibase.js create mode 100644 test/spec/services/osm_wikibase.js diff --git a/modules/services/index.js b/modules/services/index.js index 59c9d9524..cc2a3d729 100644 --- a/modules/services/index.js +++ b/modules/services/index.js @@ -3,6 +3,7 @@ import serviceMapRules from './maprules'; import serviceNominatim from './nominatim'; import serviceOpenstreetcam from './openstreetcam'; import serviceOsm from './osm'; +import serviceOsmWikibase from './osm_wikibase'; import serviceStreetside from './streetside'; import serviceTaginfo from './taginfo'; import serviceVectorTile from './vector_tile'; @@ -15,6 +16,7 @@ export var services = { mapillary: serviceMapillary, openstreetcam: serviceOpenstreetcam, osm: serviceOsm, + osmWikibase: serviceOsmWikibase, maprules: serviceMapRules, streetside: serviceStreetside, taginfo: serviceTaginfo, @@ -29,6 +31,7 @@ export { serviceNominatim, serviceOpenstreetcam, serviceOsm, + serviceOsmWikibase, serviceStreetside, serviceTaginfo, serviceVectorTile, diff --git a/modules/services/osm_wikibase.js b/modules/services/osm_wikibase.js new file mode 100644 index 000000000..ef1d2eafa --- /dev/null +++ b/modules/services/osm_wikibase.js @@ -0,0 +1,74 @@ +import _debounce from 'lodash-es/debounce'; +import _forEach from 'lodash-es/forEach'; + +import { json as d3_json } from 'd3-request'; + +import { utilQsString } from '../util'; +import { currentLocale } from '../util/locale'; + + +var apibase = 'https://wiki.openstreetmap.org/w/api.php'; +var _inflight = {}; +var _wikibaseCache = {}; + + +var debouncedRequest = _debounce(request, 500, { leading: false }); + +function request(url, callback) { + if (_inflight[url]) return; + + _inflight[url] = d3_json(url, function (err, data) { + delete _inflight[url]; + callback(err, data); + }); +} + + +export default { + + init: function() { + _inflight = {}; + _wikibaseCache = {}; + }, + + + reset: function() { + _forEach(_inflight, function(req) { req.abort(); }); + _inflight = {}; + }, + + + docs: function(params, callback) { + var doRequest = params.debounce ? debouncedRequest : request; + + // if (params.value) path = 'tag/wiki_pages?'; + // else if (params.rtype) path = 'relation/wiki_pages?'; + + var obj = { + action: 'wbgetentities', + sites: 'wiki', + titles: 'Tag:amenity=parking', + languages: 'en', + origin: '*', + formatversion: 2, + format: 'json' + } + var url = apibase + '?' + utilQsString(obj); + doRequest(url, function(err, d) { + if (err) { + callback(err); + } else { + _wikibaseCache[url] = d.data; + callback(null, d.data); + } + }); + }, + + + apibase: function(_) { + if (!arguments.length) return apibase; + apibase = _; + return this; + } + +}; diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js index 03f625019..a1dcf15d9 100644 --- a/modules/ui/tag_reference.js +++ b/modules/ui/tag_reference.js @@ -13,7 +13,8 @@ import { svgIcon } from '../svg'; export function uiTagReference(tag) { - var taginfo = services.taginfo; + // var taginfo = services.taginfo; + var wikibase = services.osmWikibase; var tagReference = {}; var _button = d3_select(null); @@ -22,44 +23,45 @@ export function uiTagReference(tag) { var _showing; - function findLocal(data) { - var locale = utilDetect().locale.toLowerCase(); - var localized; + // function findLocal(data) { + // var locale = utilDetect().locale.toLowerCase(); + // var localized; - if (locale !== 'pt-br') { // see #3776, prefer 'pt' over 'pt-br' - localized = _find(data, function(d) { - return d.lang.toLowerCase() === locale; - }); - if (localized) return localized; - } + // if (locale !== 'pt-br') { // see #3776, prefer 'pt' over 'pt-br' + // localized = _find(data, function(d) { + // return d.lang.toLowerCase() === locale; + // }); + // if (localized) return localized; + // } - // try the non-regional version of a language, like - // 'en' if the language is 'en-US' - if (locale.indexOf('-') !== -1) { - var first = locale.split('-')[0]; - localized = _find(data, function(d) { - return d.lang.toLowerCase() === first; - }); - if (localized) return localized; - } + // // try the non-regional version of a language, like + // // 'en' if the language is 'en-US' + // if (locale.indexOf('-') !== -1) { + // var first = locale.split('-')[0]; + // localized = _find(data, function(d) { + // return d.lang.toLowerCase() === first; + // }); + // if (localized) return localized; + // } - // finally fall back to english - return _find(data, function(d) { - return d.lang.toLowerCase() === 'en'; - }); - } + // // finally fall back to english + // return _find(data, function(d) { + // return d.lang.toLowerCase() === 'en'; + // }); + // } function load(param) { - if (!taginfo) return; + if (!wikibase) return; _button .classed('tag-reference-loading', true); - taginfo.docs(param, function show(err, data) { + wikibase.docs(param, function show(err, data) { var docs; if (!err && data) { - docs = findLocal(data); + // docs = findLocal(data); + debugger; } _body.html(''); diff --git a/test/spec/services/osm_wikibase.js b/test/spec/services/osm_wikibase.js new file mode 100644 index 000000000..1c87a6f8e --- /dev/null +++ b/test/spec/services/osm_wikibase.js @@ -0,0 +1,49 @@ +describe('iD.serviceOsmWikibase', function() { + var server, wikibase; + + + before(function() { + iD.services.osmWikibase = iD.serviceOsmWikibase; + }); + + after(function() { + delete iD.services.osmWikibase; + }); + + beforeEach(function() { + wikibase = iD.services.osmWikibase; + wikibase.init(); + server = sinon.fakeServer.create(); + }); + + afterEach(function() { + server.restore(); + }); + + + function query(url) { + return iD.utilStringQs(url.substring(url.indexOf('?') + 1)); + } + + + describe('#docs', function() { + it('calls the given callback with the results of the docs query', function() { + var callback = sinon.spy(); + wikibase.docs({key: 'amenity', value: 'parking'}, callback); + + server.respondWith('GET', /\/tag\/wiki_page/, + [200, { 'Content-Type': 'application/json' }, + '{"data":[{"on_way":false,"lang":"en","on_area":true,"image":"File:Car park2.jpg"}]}'] + ); + server.respond(); + + expect(query(server.requests[0].url)).to.eql( + {key: 'amenity', value: 'parking'} + ); + expect(callback).to.have.been.calledWith( + null, [{'on_way':false,'lang':'en','on_area':true,'image':'File:Car park2.jpg'}] + ); + }); + }); + +}); From b2810105a5cd6d5b13df6a720c8fc8c23edf3eff Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 22 Dec 2018 00:19:10 -0500 Subject: [PATCH 2/5] Implement support for multilingual descriptions from wiki data items * Takes data directly from the Wikibase data items (OSM Wiki) https://wiki.openstreetmap.org/wiki/OpenStreetMap:Data_Items * Understands the difference in regions - e.g. will show different images depending on the local settings * Perf: Single request will get both the tag and key description --- data/core.yaml | 2 +- dist/locales/en.json | 2 +- modules/services/osm_wikibase.js | 133 +++++++++++- modules/ui/tag_reference.js | 92 ++++---- test/index.html | 1 + test/spec/services/osm_wikibase.js | 328 +++++++++++++++++++++++++---- 6 files changed, 464 insertions(+), 94 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index c74f1a977..a06894368 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -405,7 +405,6 @@ en: inspector: no_documentation_combination: There is no documentation available for this tag combination no_documentation_key: There is no documentation available for this key - documentation_redirect: This documentation has been redirected to a new page show_more: Show More view_on_osm: View on openstreetmap.org all_fields: All fields @@ -418,6 +417,7 @@ en: choose: Select feature type results: "{n} results for {search}" reference: View on OpenStreetMap Wiki + edit_reference: Edit or translate on OSM Wiki back_tooltip: Change feature remove: Remove search: Search diff --git a/dist/locales/en.json b/dist/locales/en.json index da3671d75..f093e6e7d 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -499,7 +499,6 @@ "inspector": { "no_documentation_combination": "There is no documentation available for this tag combination", "no_documentation_key": "There is no documentation available for this key", - "documentation_redirect": "This documentation has been redirected to a new page", "show_more": "Show More", "view_on_osm": "View on openstreetmap.org", "all_fields": "All fields", @@ -512,6 +511,7 @@ "choose": "Select feature type", "results": "{n} results for {search}", "reference": "View on OpenStreetMap Wiki", + "edit_reference": "Edit or translate on OSM Wiki", "back_tooltip": "Change feature", "remove": "Remove", "search": "Search", diff --git a/modules/services/osm_wikibase.js b/modules/services/osm_wikibase.js index ef1d2eafa..22db05bd9 100644 --- a/modules/services/osm_wikibase.js +++ b/modules/services/osm_wikibase.js @@ -4,7 +4,6 @@ import _forEach from 'lodash-es/forEach'; import { json as d3_json } from 'd3-request'; import { utilQsString } from '../util'; -import { currentLocale } from '../util/locale'; var apibase = 'https://wiki.openstreetmap.org/w/api.php'; @@ -38,28 +37,140 @@ export default { }, - docs: function(params, callback) { + /** List of data items representing language regions. + * To regenerate, use Sophox query: http://tinyurl.com/y6v9ne2c (every instance of Q6999) + * A less accurate list can be seen here (everything that links to Q6999): + * https://wiki.openstreetmap.org/w/index.php?title=Special%3AWhatLinksHere&target=Item%3AQ6999&namespace=120 + */ + regionCodes: { + ar: 'Q7780', az: 'Q7781', bg: 'Q7782', bn: 'Q7783', ca: 'Q7784', cs: 'Q7785', da: 'Q7786', + de: 'Q6994', el: 'Q7787', es: 'Q7788', et: 'Q7789', fa: 'Q7790', fi: 'Q7791', fr: 'Q7792', + gl: 'Q7793', hr: 'Q7794', ht: 'Q7795', hu: 'Q7796', id: 'Q7797', it: 'Q7798', ja: 'Q7799', + ko: 'Q7800', lt: 'Q7801', lv: 'Q7802', ms: 'Q7803', nl: 'Q7804', no: 'Q7805', pl: 'Q7806', + pt: 'Q7807', ro: 'Q7808', ru: 'Q7809', sk: 'Q7810', sq: 'Q7811', sv: 'Q7812', tr: 'Q7813', + uk: 'Q7814', vi: 'Q7815', yue: 'Q7816', 'zh-hans': 'Q7817', 'zh-hant': 'Q7818', + }, + + + /** + * Get the best value for the property, or undefined if not found + * @param entity object from wikibase + * @param property string e.g. 'P4' for image + * @param langCode string e.g. 'fr' for French + */ + claimToValue: function(entity, property, langCode) { + if (!entity.claims[property]) return undefined; + var region = this.regionCodes[langCode]; + var preferredPick, regionPick; + _forEach(entity.claims[property], function(stmt) { + // If exists, use value limited to the needed language (has a qualifier P26 = region) + // Or if not found, use the first value with the "preferred" rank + if (!preferredPick && stmt.rank === 'preferred') { + preferredPick = stmt; + } + if (stmt.qualifiers && stmt.qualifiers.P26 && stmt.qualifiers.P26[0].datavalue.value.id === region) { + regionPick = stmt; + } + }); + var result = regionPick || preferredPick; + + if (result) { + var datavalue = result.mainsnak.datavalue; + return datavalue.type === 'wikibase-entityid' ? datavalue.value.id : datavalue.value; + } else { + return undefined; + } + }, + + + getDescription: function(entity) { + if (entity.descriptions) { + // Assume that there will be at most two languages because of + // how we request it: English + possibly another one. + // Pick non-English description if available (if we have more than one) + var langs = Object.keys(entity.descriptions); + if (langs.length) { + var lng = langs.length > 1 && langs[0] === 'en' ? langs[1] : langs[0]; + return entity.descriptions[lng].value; + } + } + return undefined; + }, + + + toSitelink: function(key, value) { + var result = value ? 'Tag:' + key + '=' + value : 'Key:' + key; + return result.replace(/_/g, ' ').trim(); + }, + + + getEntity: function(params, callback) { var doRequest = params.debounce ? debouncedRequest : request; - // if (params.value) path = 'tag/wiki_pages?'; - // else if (params.rtype) path = 'relation/wiki_pages?'; + var titles = []; + var languages = ['en']; + var result = {}; + var keySitelink = this.toSitelink(params.key); + var tagSitelink = params.value ? this.toSitelink(params.key, params.value) : false; + + if (params.langCode && params.langCode !== 'en') { + languages.push(params.langCode); + } + + if (_wikibaseCache[keySitelink]) { + result.key = _wikibaseCache[keySitelink]; + } else { + titles.push(keySitelink); + } + + if (tagSitelink) { + if (_wikibaseCache[tagSitelink]) { + result.key = _wikibaseCache[tagSitelink]; + } else { + titles.push(tagSitelink); + } + } + + if (!titles.length) { + // Nothing to do, we already had everything in the cache + return callback(null, result); + } var obj = { action: 'wbgetentities', sites: 'wiki', - titles: 'Tag:amenity=parking', - languages: 'en', + titles: titles.join('|'), + languages: languages.join('|'), origin: '*', - formatversion: 2, - format: 'json' - } + format: 'json', + // There is an MW Wikibase API bug https://phabricator.wikimedia.org/T212069 + // We shouldn't use v1 until it gets fixed, but should switch to it afterwards + // formatversion: 2, + }; + var url = apibase + '?' + utilQsString(obj); doRequest(url, function(err, d) { if (err) { callback(err); + } else if (!d.success || d.error) { + callback(d.error.messages.map(function(v) { return v.html['*']; }).join('
')); } else { - _wikibaseCache[url] = d.data; - callback(null, d.data); + _forEach(d.entities, function(res) { + if (res.missing !== '') { + var title = res.sitelinks.wiki.title; + if (title === keySitelink) { + _wikibaseCache[keySitelink] = res; + result.key = res; + } else if (title === tagSitelink) { + _wikibaseCache[tagSitelink] = res; + result.tag = res; + } else { + console.log('Unexpected title ' + title); + } + } + }); + + callback(null, result); } }); }, diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js index a1dcf15d9..d4ce97f5c 100644 --- a/modules/ui/tag_reference.js +++ b/modules/ui/tag_reference.js @@ -1,6 +1,3 @@ -import _find from 'lodash-es/find'; -import _omit from 'lodash-es/omit'; - import { event as d3_event, select as d3_select @@ -10,10 +7,10 @@ import { t } from '../util/locale'; import { utilDetect } from '../util/detect'; import { services } from '../services'; import { svgIcon } from '../svg'; +import { utilQsString } from '../util'; export function uiTagReference(tag) { - // var taginfo = services.taginfo; var wikibase = services.osmWikibase; var tagReference = {}; @@ -22,33 +19,40 @@ export function uiTagReference(tag) { var _loaded; var _showing; + /** + * @returns {{itemTitle: String, description: String, image: String|null}|null} + **/ + function findLocal(data) { + var entity = data.tag || data.key; + if (!entity) return null; - // function findLocal(data) { - // var locale = utilDetect().locale.toLowerCase(); - // var localized; + var result = { + title: entity.title, + description: wikibase.getDescription(entity), + }; - // if (locale !== 'pt-br') { // see #3776, prefer 'pt' over 'pt-br' - // localized = _find(data, function(d) { - // return d.lang.toLowerCase() === locale; - // }); - // if (localized) return localized; - // } + if (entity.claims) { + var langCode = utilDetect().locale.toLowerCase(); + var url; + var image = wikibase.claimToValue(entity, 'P4', langCode); + if (image) { + url = 'https://commons.wikimedia.org/w/index.php'; + } else { + image = wikibase.claimToValue(entity, 'P28', langCode); + if (image) { + url = 'https://wiki.openstreetmap.org/w/index.php'; + } + } + if (image) { + result.image = { + url: url, + title: 'Special:Redirect/file/' + image + }; + } + } - // // try the non-regional version of a language, like - // // 'en' if the language is 'en-US' - // if (locale.indexOf('-') !== -1) { - // var first = locale.split('-')[0]; - // localized = _find(data, function(d) { - // return d.lang.toLowerCase() === first; - // }); - // if (localized) return localized; - // } - - // // finally fall back to english - // return _find(data, function(d) { - // return d.lang.toLowerCase() === 'en'; - // }); - // } + return result; + } function load(param) { @@ -57,33 +61,34 @@ export function uiTagReference(tag) { _button .classed('tag-reference-loading', true); - wikibase.docs(param, function show(err, data) { + wikibase.getEntity(param, function show(err, data) { var docs; if (!err && data) { - // docs = findLocal(data); - debugger; + docs = findLocal(data); } _body.html(''); if (!docs || !docs.title) { - if (param.hasOwnProperty('value')) { - load(_omit(param, 'value')); // retry with key only - } else { - _body - .append('p') - .attr('class', 'tag-reference-description') - .text(t('inspector.no_documentation_key')); - done(); - } + _body + .append('p') + .attr('class', 'tag-reference-description') + .text(t('inspector.no_documentation_key')); + done(); return; } - if (docs.image && docs.image.thumb_url_prefix) { + if (docs.image) { + var imageUrl = docs.image.url + '?' + utilQsString({ + title: docs.image.title, + width: 100, + height: 100, + }); + _body .append('img') .attr('class', 'tag-reference-wiki-image') - .attr('src', docs.image.thumb_url_prefix + '100' + docs.image.thumb_url_suffix) + .attr('src', imageUrl) .on('load', function() { done(); }) .on('error', function() { d3_select(this).remove(); done(); }); } else { @@ -93,7 +98,7 @@ export function uiTagReference(tag) { _body .append('p') .attr('class', 'tag-reference-description') - .text(docs.description || t('inspector.documentation_redirect')); + .text(docs.description || t('inspector.no_documentation_key')); _body .append('a') @@ -173,6 +178,7 @@ export function uiTagReference(tag) { } else if (_loaded) { done(); } else { + tag.langCode = utilDetect().locale.toLowerCase(); load(tag); } }); diff --git a/test/index.html b/test/index.html index b455624b3..8e78ab96d 100644 --- a/test/index.html +++ b/test/index.html @@ -110,6 +110,7 @@ + diff --git a/test/spec/services/osm_wikibase.js b/test/spec/services/osm_wikibase.js index 1c87a6f8e..2536fd246 100644 --- a/test/spec/services/osm_wikibase.js +++ b/test/spec/services/osm_wikibase.js @@ -1,49 +1,301 @@ -describe('iD.serviceOsmWikibase', function() { - var server, wikibase; +describe('iD.serviceOsmWikibase', function () { + var server, wikibase; + + before(function () { + iD.services.osmWikibase = iD.serviceOsmWikibase; + }); + + after(function () { + delete iD.services.osmWikibase; + }); + + beforeEach(function () { + wikibase = iD.services.osmWikibase; + wikibase.init(); + server = sinon.fakeServer.create(); + }); + + afterEach(function () { + server.restore(); + }); - before(function() { - iD.services.osmWikibase = iD.serviceOsmWikibase; - }); + function query(url) { + return iD.utilStringQs(url.substring(url.indexOf('?') + 1)); + } - after(function() { - delete iD.services.osmWikibase; - }); - - beforeEach(function() { - wikibase = iD.services.osmWikibase; - wikibase.init(); - server = sinon.fakeServer.create(); - }); - - afterEach(function() { - server.restore(); - }); - - - function query(url) { - return iD.utilStringQs(url.substring(url.indexOf('?') + 1)); + var keyData = { + pageid: 205725, + ns: 120, + title: 'Item:Q61', + lastrevid: 1721242, + modified: '2018-12-18T07:00:43Z', + type: 'item', + id: 'Q61', + labels: { + en: {language: 'en', value: 'amenity'} + }, + descriptions: { + en: {language: 'en', value: 'For describing useful and important facilities for visitors and residents.'} + }, + aliases: {}, + claims: { + P2: [ // instance of + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q7'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P16: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'string', + datavalue: {value: 'amenity', type: 'string'} + }, + type: 'statement', + rank: 'normal' + } + ], + P25: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q4679'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P9: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q8'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P6: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q15'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'preferred' + }, + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q14'}, type: 'wikibase-entityid'} + }, + type: 'statement', + qualifiers: { + P26: [ + { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q6994'}, type: 'wikibase-entityid'} + } + ] + }, + rank: 'normal' + } + ], + P28: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'string', + datavalue: {value: 'Mapping-Features-Parking-Lot.png', type: 'string'} + }, + type: 'statement', + rank: 'normal' + } + ] + }, + sitelinks: { + wiki: { + site: 'wiki', + title: 'Key:amenity', + badges: [] + } } + }; + var tagData = { + pageid: 210934, + ns: 120, + title: 'Item:Q4904', + lastrevid: 1718041, + modified: '2018-12-18T03:51:05Z', + type: 'item', + id: 'Q4904', + labels: { + en: {language: 'en', value: 'amenity=parking'} + }, + descriptions: { + en: {language: 'en', value: 'A place for parking cars'}, + fr: {language: 'fr', value: 'Un lieu pour garer des voitures'} + }, + aliases: {}, + claims: { + P2: [ // instance of = Q2 (tag) + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q2'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P19: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'string', + datavalue: {value: 'amenity=parking', type: 'string'} + }, + type: 'statement', + rank: 'normal' + } + ], + P10: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q61'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P4: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'commonsMedia', + datavalue: {value: 'Primary image.jpg', type: 'string'} + }, + type: 'statement', + rank: 'preferred' + } + ], + P6: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q14'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'preferred' + }, + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q13'}, type: 'wikibase-entityid'} + }, + type: 'statement', + qualifiers: { + P26: [ + { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q6994'}, type: 'wikibase-entityid'} + } + ] + }, + rank: 'normal' + } + ], + P25: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q4679'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ] + }, + sitelinks: { + wiki: { + site: 'wiki', + title: 'Tag:amenity=parking', + badges: [] + } + } + }; - describe('#docs', function() { - it('calls the given callback with the results of the docs query', function() { - var callback = sinon.spy(); - wikibase.docs({key: 'amenity', value: 'parking'}, callback); + describe('#getEntity', function () { + it('calls the given callback with the results of the getEntity data item query', function () { + var callback = sinon.spy(); + wikibase.getEntity({key: 'amenity', value: 'parking', langCode: 'fr'}, callback); - server.respondWith('GET', /\/tag\/wiki_page/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"on_way":false,"lang":"en","on_area":true,"image":"File:Car park2.jpg"}]}'] - ); - server.respond(); + server.respondWith('GET', /action=wbgetentities/, + [200, {'Content-Type': 'application/json'}, JSON.stringify({ + entities: { + Q61: keyData, + Q4904: tagData + }, + success: 1 + })] + ); + server.respond(); - expect(query(server.requests[0].url)).to.eql( - {key: 'amenity', value: 'parking'} - ); - expect(callback).to.have.been.calledWith( - null, [{'on_way':false,'lang':'en','on_area':true,'image':'File:Car park2.jpg'}] - ); - }); + expect(query(server.requests[0].url)).to.eql( + { + action: 'wbgetentities', + format: 'json', + languages: 'en|fr', + origin: '*', + sites: 'wiki', + titles: 'Key:amenity|Tag:amenity=parking', + } + ); + expect(callback).to.have.been.calledWith(null, {key: keyData, tag: tagData}); }); + }); + + + it('creates correct sitelinks', function () { + expect(wikibase.toSitelink('amenity')).to.eql('Key:amenity'); + expect(wikibase.toSitelink('amenity_')).to.eql('Key:amenity'); + expect(wikibase.toSitelink('_amenity_')).to.eql('Key: amenity'); + expect(wikibase.toSitelink('amenity or_not_')).to.eql('Key:amenity or not'); + expect(wikibase.toSitelink('amenity', 'parking')).to.eql('Tag:amenity=parking'); + expect(wikibase.toSitelink(' amenity_', '_parking_')).to.eql('Tag: amenity = parking'); + expect(wikibase.toSitelink('amenity or_not', '_park ing_')).to.eql('Tag:amenity or not= park ing'); + }); + + it('gets correct value from entity', function () { + expect(wikibase.claimToValue(tagData, 'P4', 'en')).to.eql('Primary image.jpg'); + expect(wikibase.claimToValue(keyData, 'P6', 'en')).to.eql('Q15'); + expect(wikibase.claimToValue(keyData, 'P6', 'fr')).to.eql('Q15'); + expect(wikibase.claimToValue(keyData, 'P6', 'de')).to.eql('Q14'); + }); + + it('gets correct description from entity', function () { + expect(wikibase.getDescription(tagData)).to.eql('Un lieu pour garer des voitures'); + expect(wikibase.getDescription(keyData)).to.eql('For describing useful and important facilities for visitors and residents.'); + }); }); From aa78cd10a3d233222a3cdc0469152af3915dde9b Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 22 Dec 2018 14:58:15 -0500 Subject: [PATCH 3/5] Data items - remove hardcoded locale IDs Locale IDs are now fetched together with the other data when the language code is provided. --- modules/services/osm_wikibase.js | 54 ++++++++++++++++++------------ test/spec/services/osm_wikibase.js | 13 +++++-- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/modules/services/osm_wikibase.js b/modules/services/osm_wikibase.js index 22db05bd9..7570c5a56 100644 --- a/modules/services/osm_wikibase.js +++ b/modules/services/osm_wikibase.js @@ -9,6 +9,7 @@ import { utilQsString } from '../util'; var apibase = 'https://wiki.openstreetmap.org/w/api.php'; var _inflight = {}; var _wikibaseCache = {}; +var _localeIds = {}; var debouncedRequest = _debounce(request, 500, { leading: false }); @@ -28,6 +29,7 @@ export default { init: function() { _inflight = {}; _wikibaseCache = {}; + _localeIds = {}; }, @@ -37,21 +39,6 @@ export default { }, - /** List of data items representing language regions. - * To regenerate, use Sophox query: http://tinyurl.com/y6v9ne2c (every instance of Q6999) - * A less accurate list can be seen here (everything that links to Q6999): - * https://wiki.openstreetmap.org/w/index.php?title=Special%3AWhatLinksHere&target=Item%3AQ6999&namespace=120 - */ - regionCodes: { - ar: 'Q7780', az: 'Q7781', bg: 'Q7782', bn: 'Q7783', ca: 'Q7784', cs: 'Q7785', da: 'Q7786', - de: 'Q6994', el: 'Q7787', es: 'Q7788', et: 'Q7789', fa: 'Q7790', fi: 'Q7791', fr: 'Q7792', - gl: 'Q7793', hr: 'Q7794', ht: 'Q7795', hu: 'Q7796', id: 'Q7797', it: 'Q7798', ja: 'Q7799', - ko: 'Q7800', lt: 'Q7801', lv: 'Q7802', ms: 'Q7803', nl: 'Q7804', no: 'Q7805', pl: 'Q7806', - pt: 'Q7807', ro: 'Q7808', ru: 'Q7809', sk: 'Q7810', sq: 'Q7811', sv: 'Q7812', tr: 'Q7813', - uk: 'Q7814', vi: 'Q7815', yue: 'Q7816', 'zh-hans': 'Q7817', 'zh-hant': 'Q7818', - }, - - /** * Get the best value for the property, or undefined if not found * @param entity object from wikibase @@ -60,19 +47,21 @@ export default { */ claimToValue: function(entity, property, langCode) { if (!entity.claims[property]) return undefined; - var region = this.regionCodes[langCode]; - var preferredPick, regionPick; + var locale = _localeIds[langCode]; + var preferredPick, localePick; _forEach(entity.claims[property], function(stmt) { - // If exists, use value limited to the needed language (has a qualifier P26 = region) + // If exists, use value limited to the needed language (has a qualifier P26 = locale) // Or if not found, use the first value with the "preferred" rank if (!preferredPick && stmt.rank === 'preferred') { preferredPick = stmt; } - if (stmt.qualifiers && stmt.qualifiers.P26 && stmt.qualifiers.P26[0].datavalue.value.id === region) { - regionPick = stmt; + if (locale && stmt.qualifiers && stmt.qualifiers.P26 && + stmt.qualifiers.P26[0].datavalue.value.id === locale + ) { + localePick = stmt; } }); - var result = regionPick || preferredPick; + var result = localePick || preferredPick; if (result) { var datavalue = result.mainsnak.datavalue; @@ -106,15 +95,23 @@ export default { getEntity: function(params, callback) { var doRequest = params.debounce ? debouncedRequest : request; - + var self = this; var titles = []; var languages = ['en']; var result = {}; var keySitelink = this.toSitelink(params.key); var tagSitelink = params.value ? this.toSitelink(params.key, params.value) : false; + var localeSitelink; if (params.langCode && params.langCode !== 'en') { languages.push(params.langCode); + if (!_localeIds[params.langCode]) { + // This is the first time we are asking about this locale + // Fetch corresponding entity (if it exists), and cache it. + // If there is no such entry, cache `true` to avoid re-requesting it. + localeSitelink = ('Locale:' + params.langCode).replace(/_/g, ' ').trim(); + titles.push(localeSitelink); + } } if (_wikibaseCache[keySitelink]) { @@ -155,6 +152,7 @@ export default { } else if (!d.success || d.error) { callback(d.error.messages.map(function(v) { return v.html['*']; }).join('
')); } else { + var localeId = true; _forEach(d.entities, function(res) { if (res.missing !== '') { var title = res.sitelinks.wiki.title; @@ -164,18 +162,30 @@ export default { } else if (title === tagSitelink) { _wikibaseCache[tagSitelink] = res; result.tag = res; + } else if (title === localeSitelink) { + localeId = res.id; } else { console.log('Unexpected title ' + title); } } }); + if (localeSitelink) { + // If locale ID is not found, set cache to true to prevent repeated queries + self.addLocale(params.langCode, localeId); + } + callback(null, result); } }); }, + addLocale: function(langCode, qid) { + // Makes it easier to unit test + _localeIds[langCode] = qid; + }, + apibase: function(_) { if (!arguments.length) return apibase; apibase = _; diff --git a/test/spec/services/osm_wikibase.js b/test/spec/services/osm_wikibase.js index 2536fd246..aaccc16f4 100644 --- a/test/spec/services/osm_wikibase.js +++ b/test/spec/services/osm_wikibase.js @@ -245,6 +245,12 @@ describe('iD.serviceOsmWikibase', function () { } }; + + var localeData = { + id: 'Q7792', + sitelinks: {wiki: {site: 'wiki', title: 'Locale:fr'}} + }; + describe('#getEntity', function () { it('calls the given callback with the results of the getEntity data item query', function () { var callback = sinon.spy(); @@ -254,7 +260,8 @@ describe('iD.serviceOsmWikibase', function () { [200, {'Content-Type': 'application/json'}, JSON.stringify({ entities: { Q61: keyData, - Q4904: tagData + Q4904: tagData, + Q7792: localeData, }, success: 1 })] @@ -268,7 +275,7 @@ describe('iD.serviceOsmWikibase', function () { languages: 'en|fr', origin: '*', sites: 'wiki', - titles: 'Key:amenity|Tag:amenity=parking', + titles: 'Locale:fr|Key:amenity|Tag:amenity=parking', } ); expect(callback).to.have.been.calledWith(null, {key: keyData, tag: tagData}); @@ -287,6 +294,8 @@ describe('iD.serviceOsmWikibase', function () { }); it('gets correct value from entity', function () { + wikibase.addLocale('de', 'Q6994'); + wikibase.addLocale('fr', 'Q7792'); expect(wikibase.claimToValue(tagData, 'P4', 'en')).to.eql('Primary image.jpg'); expect(wikibase.claimToValue(keyData, 'P6', 'en')).to.eql('Q15'); expect(wikibase.claimToValue(keyData, 'P6', 'fr')).to.eql('Q15'); From 8b931f3e6c50bc447f39d469aadf814332d8a775 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 22 Dec 2018 15:03:22 -0500 Subject: [PATCH 4/5] Use new message to encourage editing and translation --- modules/ui/tag_reference.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js index d4ce97f5c..8fa05a1b8 100644 --- a/modules/ui/tag_reference.js +++ b/modules/ui/tag_reference.js @@ -108,7 +108,7 @@ export function uiTagReference(tag) { .attr('href', 'https://wiki.openstreetmap.org/wiki/' + docs.title) .call(svgIcon('#iD-icon-out-link', 'inline')) .append('span') - .text(t('inspector.reference')); + .text(t('inspector.edit_reference')); // Add link to info about "good changeset comments" - #2923 if (param.key === 'comment') { From 9f7f4aa3fcf95eb3ceb86e9965ee286d1d94f0c0 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 23 Dec 2018 01:04:35 -0500 Subject: [PATCH 5/5] Fix language fallback processing, caching bug. --- modules/services/osm_wikibase.js | 66 ++-- modules/ui/tag_reference.js | 2 +- test/spec/services/osm_wikibase.js | 470 +++++++++++++++-------------- 3 files changed, 278 insertions(+), 260 deletions(-) diff --git a/modules/services/osm_wikibase.js b/modules/services/osm_wikibase.js index 7570c5a56..366f6c5fc 100644 --- a/modules/services/osm_wikibase.js +++ b/modules/services/osm_wikibase.js @@ -9,7 +9,7 @@ import { utilQsString } from '../util'; var apibase = 'https://wiki.openstreetmap.org/w/api.php'; var _inflight = {}; var _wikibaseCache = {}; -var _localeIds = {}; +var _localeIds = { en: false }; var debouncedRequest = _debounce(request, 500, { leading: false }); @@ -24,6 +24,22 @@ function request(url, callback) { } +/** + * Get the best string value from the descriptions/labels result + * Note that if mediawiki doesn't recognize language code, it will return all values. + * In that case, fallback to use English. + * @param values object - either descriptions or labels + * @param langCode String + * @returns localized string + */ +function localizedToString(values, langCode) { + if (values) { + values = values[langCode] || values.en; + } + return values ? values.value : ''; +} + + export default { init: function() { @@ -72,21 +88,6 @@ export default { }, - getDescription: function(entity) { - if (entity.descriptions) { - // Assume that there will be at most two languages because of - // how we request it: English + possibly another one. - // Pick non-English description if available (if we have more than one) - var langs = Object.keys(entity.descriptions); - if (langs.length) { - var lng = langs.length > 1 && langs[0] === 'en' ? langs[1] : langs[0]; - return entity.descriptions[lng].value; - } - } - return undefined; - }, - - toSitelink: function(key, value) { var result = value ? 'Tag:' + key + '=' + value : 'Key:' + key; return result.replace(/_/g, ' ').trim(); @@ -97,21 +98,17 @@ export default { var doRequest = params.debounce ? debouncedRequest : request; var self = this; var titles = []; - var languages = ['en']; var result = {}; var keySitelink = this.toSitelink(params.key); var tagSitelink = params.value ? this.toSitelink(params.key, params.value) : false; var localeSitelink; - if (params.langCode && params.langCode !== 'en') { - languages.push(params.langCode); - if (!_localeIds[params.langCode]) { - // This is the first time we are asking about this locale - // Fetch corresponding entity (if it exists), and cache it. - // If there is no such entry, cache `true` to avoid re-requesting it. - localeSitelink = ('Locale:' + params.langCode).replace(/_/g, ' ').trim(); - titles.push(localeSitelink); - } + if (params.langCode && _localeIds[params.langCode] === undefined) { + // If this is the first time we are asking about this locale, + // fetch corresponding entity (if it exists), and cache it. + // If there is no such entry, cache `false` value to avoid re-requesting it. + localeSitelink = ('Locale:' + params.langCode).replace(/_/g, ' ').trim(); + titles.push(localeSitelink); } if (_wikibaseCache[keySitelink]) { @@ -122,7 +119,7 @@ export default { if (tagSitelink) { if (_wikibaseCache[tagSitelink]) { - result.key = _wikibaseCache[tagSitelink]; + result.tag = _wikibaseCache[tagSitelink]; } else { titles.push(tagSitelink); } @@ -133,11 +130,17 @@ export default { return callback(null, result); } + // Requesting just the user language code + // If backend recognizes the code, it will perform proper fallbacks, + // and the result will contain the requested code. If not, all values are returned: + // {"zh-tw":{"value":"...","language":"zh-tw","source-language":"zh-hant"} + // {"pt-br":{"value":"...","language":"pt","for-language":"pt-br"}} var obj = { action: 'wbgetentities', sites: 'wiki', titles: titles.join('|'), - languages: languages.join('|'), + languages: params.langCode, + languagefallback: 1, origin: '*', format: 'json', // There is an MW Wikibase API bug https://phabricator.wikimedia.org/T212069 @@ -152,10 +155,13 @@ export default { } else if (!d.success || d.error) { callback(d.error.messages.map(function(v) { return v.html['*']; }).join('
')); } else { - var localeId = true; + var localeId = false; _forEach(d.entities, function(res) { if (res.missing !== '') { var title = res.sitelinks.wiki.title; + // Simplify access to the localized values + res.description = localizedToString(res.descriptions, params.langCode); + res.label = localizedToString(res.labels, params.langCode); if (title === keySitelink) { _wikibaseCache[keySitelink] = res; result.key = res; @@ -171,7 +177,7 @@ export default { }); if (localeSitelink) { - // If locale ID is not found, set cache to true to prevent repeated queries + // If locale ID is not found, store false to prevent repeated queries self.addLocale(params.langCode, localeId); } diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js index 8fa05a1b8..6215250b9 100644 --- a/modules/ui/tag_reference.js +++ b/modules/ui/tag_reference.js @@ -28,7 +28,7 @@ export function uiTagReference(tag) { var result = { title: entity.title, - description: wikibase.getDescription(entity), + description: entity.description, }; if (entity.claims) { diff --git a/test/spec/services/osm_wikibase.js b/test/spec/services/osm_wikibase.js index aaccc16f4..61951abba 100644 --- a/test/spec/services/osm_wikibase.js +++ b/test/spec/services/osm_wikibase.js @@ -24,226 +24,239 @@ describe('iD.serviceOsmWikibase', function () { return iD.utilStringQs(url.substring(url.indexOf('?') + 1)); } - var keyData = { - pageid: 205725, - ns: 120, - title: 'Item:Q61', - lastrevid: 1721242, - modified: '2018-12-18T07:00:43Z', - type: 'item', - id: 'Q61', - labels: { - en: {language: 'en', value: 'amenity'} - }, - descriptions: { - en: {language: 'en', value: 'For describing useful and important facilities for visitors and residents.'} - }, - aliases: {}, - claims: { - P2: [ // instance of - { - mainsnak: { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q7'}, type: 'wikibase-entityid'} - }, - type: 'statement', - rank: 'normal' - } - ], - P16: [ - { - mainsnak: { - snaktype: 'value', - datatype: 'string', - datavalue: {value: 'amenity', type: 'string'} - }, - type: 'statement', - rank: 'normal' - } - ], - P25: [ - { - mainsnak: { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q4679'}, type: 'wikibase-entityid'} - }, - type: 'statement', - rank: 'normal' - } - ], - P9: [ - { - mainsnak: { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q8'}, type: 'wikibase-entityid'} - }, - type: 'statement', - rank: 'normal' - } - ], - P6: [ - { - mainsnak: { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q15'}, type: 'wikibase-entityid'} - }, - type: 'statement', - rank: 'preferred' - }, - { - mainsnak: { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q14'}, type: 'wikibase-entityid'} - }, - type: 'statement', - qualifiers: { - P26: [ - { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q6994'}, type: 'wikibase-entityid'} - } - ] - }, - rank: 'normal' - } - ], - P28: [ - { - mainsnak: { - snaktype: 'value', - datatype: 'string', - datavalue: {value: 'Mapping-Features-Parking-Lot.png', type: 'string'} - }, - type: 'statement', - rank: 'normal' - } - ] - }, - sitelinks: { - wiki: { - site: 'wiki', - title: 'Key:amenity', - badges: [] + function adjust(params, data) { + if (params) { + if (params.norm) { + data.description = data.descriptions.fr.value; + data.label = data.labels.fr.value; } } - }; + return data; + } - var tagData = { - pageid: 210934, - ns: 120, - title: 'Item:Q4904', - lastrevid: 1718041, - modified: '2018-12-18T03:51:05Z', - type: 'item', - id: 'Q4904', - labels: { - en: {language: 'en', value: 'amenity=parking'} - }, - descriptions: { - en: {language: 'en', value: 'A place for parking cars'}, - fr: {language: 'fr', value: 'Un lieu pour garer des voitures'} - }, - aliases: {}, - claims: { - P2: [ // instance of = Q2 (tag) - { - mainsnak: { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q2'}, type: 'wikibase-entityid'} + function keyData(params) { + return adjust(params, { + pageid: 205725, + ns: 120, + title: 'Item:Q42', + lastrevid: 1721242, + modified: '2018-12-18T07:00:43Z', + type: 'item', + id: 'Q42', + labels: { + fr: {language: 'en', value: 'amenity', 'for-language': 'fr'} + }, + descriptions: { + fr: {language: 'en', value: 'English description', 'for-language': 'fr'} + }, + aliases: {}, + claims: { + P2: [ // instance of + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q7'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P16: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'string', + datavalue: {value: 'amenity', type: 'string'} + }, + type: 'statement', + rank: 'normal' + } + ], + P25: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q4679'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P9: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q8'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P6: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q15'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'preferred' }, - type: 'statement', - rank: 'normal' + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q14'}, type: 'wikibase-entityid'} + }, + type: 'statement', + qualifiers: { + P26: [ + { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q6994'}, type: 'wikibase-entityid'} + } + ] + }, + rank: 'normal' + } + ], + P28: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'string', + datavalue: {value: 'Mapping-Features-Parking-Lot.png', type: 'string'} + }, + type: 'statement', + rank: 'normal' + } + ] + }, + sitelinks: { + wiki: { + site: 'wiki', + title: 'Key:amenity', + badges: [] } - ], - P19: [ - { - mainsnak: { - snaktype: 'value', - datatype: 'string', - datavalue: {value: 'amenity=parking', type: 'string'} - }, - type: 'statement', - rank: 'normal' - } - ], - P10: [ - { - mainsnak: { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q61'}, type: 'wikibase-entityid'} - }, - type: 'statement', - rank: 'normal' - } - ], - P4: [ - { - mainsnak: { - snaktype: 'value', - datatype: 'commonsMedia', - datavalue: {value: 'Primary image.jpg', type: 'string'} - }, - type: 'statement', - rank: 'preferred' - } - ], - P6: [ - { - mainsnak: { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q14'}, type: 'wikibase-entityid'} - }, - type: 'statement', - rank: 'preferred' - }, - { - mainsnak: { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q13'}, type: 'wikibase-entityid'} - }, - type: 'statement', - qualifiers: { - P26: [ - { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q6994'}, type: 'wikibase-entityid'} - } - ] - }, - rank: 'normal' - } - ], - P25: [ - { - mainsnak: { - snaktype: 'value', - datatype: 'wikibase-item', - datavalue: {value: {'entity-type': 'item', id: 'Q4679'}, type: 'wikibase-entityid'} - }, - type: 'statement', - rank: 'normal' - } - ] - }, - sitelinks: { - wiki: { - site: 'wiki', - title: 'Tag:amenity=parking', - badges: [] } - } - }; + }); + } + + function tagData(params) { + return adjust(params, { + pageid: 210934, + ns: 120, + title: 'Item:Q13', + lastrevid: 1718041, + modified: '2018-12-18T03:51:05Z', + type: 'item', + id: 'Q13', + labels: { + fr: {language: 'en', value: 'amenity=parking', 'for-language': 'fr'} + }, + descriptions: { + fr: {language: 'fr', value: 'French description'} + }, + aliases: {}, + claims: { + P2: [ // instance of = Q2 (tag) + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q2'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P19: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'string', + datavalue: {value: 'amenity=parking', type: 'string'} + }, + type: 'statement', + rank: 'normal' + } + ], + P10: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q42'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ], + P4: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'commonsMedia', + datavalue: {value: 'Primary image.jpg', type: 'string'} + }, + type: 'statement', + rank: 'preferred' + } + ], + P6: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q14'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'preferred' + }, + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q13'}, type: 'wikibase-entityid'} + }, + type: 'statement', + qualifiers: { + P26: [ + { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q6994'}, type: 'wikibase-entityid'} + } + ] + }, + rank: 'normal' + } + ], + P25: [ + { + mainsnak: { + snaktype: 'value', + datatype: 'wikibase-item', + datavalue: {value: {'entity-type': 'item', id: 'Q4679'}, type: 'wikibase-entityid'} + }, + type: 'statement', + rank: 'normal' + } + ] + }, + sitelinks: { + wiki: { + site: 'wiki', + title: 'Tag:amenity=parking', + badges: [] + } + } + }); + } var localeData = { @@ -259,8 +272,8 @@ describe('iD.serviceOsmWikibase', function () { server.respondWith('GET', /action=wbgetentities/, [200, {'Content-Type': 'application/json'}, JSON.stringify({ entities: { - Q61: keyData, - Q4904: tagData, + Q42: keyData(), + Q13: tagData(), Q7792: localeData, }, success: 1 @@ -271,14 +284,18 @@ describe('iD.serviceOsmWikibase', function () { expect(query(server.requests[0].url)).to.eql( { action: 'wbgetentities', - format: 'json', - languages: 'en|fr', - origin: '*', sites: 'wiki', titles: 'Locale:fr|Key:amenity|Tag:amenity=parking', + languages: 'fr', + languagefallback: '1', + origin: '*', + format: 'json', } ); - expect(callback).to.have.been.calledWith(null, {key: keyData, tag: tagData}); + expect(callback).to.have.been.calledWith(null, { + key: keyData({norm: true}), + tag: tagData({norm: true}) + }); }); }); @@ -296,15 +313,10 @@ describe('iD.serviceOsmWikibase', function () { it('gets correct value from entity', function () { wikibase.addLocale('de', 'Q6994'); wikibase.addLocale('fr', 'Q7792'); - expect(wikibase.claimToValue(tagData, 'P4', 'en')).to.eql('Primary image.jpg'); - expect(wikibase.claimToValue(keyData, 'P6', 'en')).to.eql('Q15'); - expect(wikibase.claimToValue(keyData, 'P6', 'fr')).to.eql('Q15'); - expect(wikibase.claimToValue(keyData, 'P6', 'de')).to.eql('Q14'); - }); - - it('gets correct description from entity', function () { - expect(wikibase.getDescription(tagData)).to.eql('Un lieu pour garer des voitures'); - expect(wikibase.getDescription(keyData)).to.eql('For describing useful and important facilities for visitors and residents.'); + expect(wikibase.claimToValue(tagData(), 'P4', 'en')).to.eql('Primary image.jpg'); + expect(wikibase.claimToValue(keyData(), 'P6', 'en')).to.eql('Q15'); + expect(wikibase.claimToValue(keyData(), 'P6', 'fr')).to.eql('Q15'); + expect(wikibase.claimToValue(keyData(), 'P6', 'de')).to.eql('Q14'); }); });