Page MenuHomePhabricator

Support formatters relying on the qualifier in the AuthorityControl gadget
Open, MediumPublic

Description

The property catalog code (P528) uses qualifer catalog (P972) to specify to which catalog the item belongs to. These catalogs hold their own formatter URLs, so the gadget should be able to find them and link the number accordingly.

Example: Messier 86 (Q2577) has the property with value "M 86" and catalog Messier object (Q14530) which has a formatter the value could be linked by.

Original proposal: https://www.wikidata.org/wiki/MediaWiki_talk:Gadget-AuthorityControl.js#Read_P972

Event Timeline

This is a substantial change. I'm not opposed to this in general (although I raised concerns about modeling at https://www.wikidata.org/wiki/MediaWiki_talk:Gadget-AuthorityControl.js#Performance_improvements_.28please_review.29), but would like some discussion. Whoever ends up implementing this, please make sure you have someone review any changes done in order to resolve this task (especially in regards to performance).

AuthorityControl is running on every page view and already is a performance bottleneck.

P3300: Not tested yet, just proposal:

1/* AuthorityControl.js
2 * Provides a link to various Authority Control tools (VIAF, GND, etc.) for Wikidata statements that
3 * feature certain properties.
4 *
5 * Original gadget coded by [[User:Ricordisamoa]]
6 */
7( function ( mw, wb, $ ) {
8'use strict';
9
10if ( ( mw.config.get( 'wgNamespaceNumber' ) !== 0 && mw.config.get( 'wgNamespaceNumber' ) !== 120 ) || !mw.config.exists( 'wbEntityId' ) ) {
11 // Only item pages feature appropriate statements.
12 return;
13}
14
15var PROPERTIES = {},
16 specialHandlingProperties = [
17 'P426', // aircraft registration
18 'P528', // catalog number
19 'P791', // ISIL ID
20 'P882' // FIPS
21 ],
22 catalogMap = {};
23
24/*
25*/
26function getGeoHackParams( coord ) {
27 // TODO: individual scale for every precision
28
29 var globes = {
30 Q2: 'earth',
31 Q111: 'mars',
32 Q308: 'mercury',
33 Q313: 'venus',
34 Q319: 'jupiter',
35 Q339: 'pluto',
36 Q405: 'moon',
37 Q596: 'ceres',
38 Q2565: 'titan',
39 Q3030: 'vesta',
40 Q3123: 'io',
41 Q3134: 'callisto',
42 Q3143: 'europa',
43 Q3169: 'ganymede',
44 Q3303: 'enceladus',
45 Q3322: 'titania',
46 Q3332: 'oberon',
47 Q3338: 'umbriel',
48 Q3343: 'ariel',
49 Q3352: 'miranda',
50 Q3359: 'triton',
51 Q7547: 'phobos',
52 Q7548: 'deimos',
53 Q15034: 'mimas',
54 Q15037: 'hyperion',
55 Q15040: 'dione',
56 Q15047: 'tethys',
57 Q15050: 'rhea',
58 Q16711: 'eros',
59 Q17958: 'iapetus',
60 Q17975: 'phoebe',
61 Q107556: 'lutetia',
62 Q158244: 'gaspra'
63 };
64
65 var globeQKey = coord.globe.replace( 'http://www.wikidata.org/entity/', '' );
66 var globe = globes[ globeQKey ];
67
68 return coord.latitude + '_N_' + coord.longitude + '_E_globe:' + globe;
69}
70
71/**
72 * Get the snak value formatted with a link.
73 *
74 * @param {string} numericPropertyId Refers to PROPERTIES.
75 * @param {string} value
76 */
77function getLinkValueForString( numericPropertyId, value ) {
78 var linkValue = value;
79
80 switch ( Number( numericPropertyId ) ) {
81 case 882: // FIPS
82 linkValue = linkValue.substr( 0, 2 ) + '/' + linkValue;
83 break;
84 }
85
86 return linkValue;
87}
88
89function makeLink( numericPropertyId, linkValue, displayText ) {
90 var linkTemplate = PROPERTIES[ numericPropertyId ];
91
92 switch ( Number( numericPropertyId ) ) {
93 case 426:
94 if ( linkValue.substring( 0, 1 ) === 'N' ) {
95 linkTemplate = 'http://registry.faa.gov/aircraftinquiry/NNum_Results.aspx?NNumbertxt=$1';
96 } else if ( linkValue.substring( 0, 2 ) === 'G-' ) {
97 linkTemplate = 'https://www.caa.co.uk/application.aspx?catid=60&pagetype=65&appid=1&mode=detailnosummary&fullregmark=$1';
98 linkValue = linkValue.substring( 2 );
99 } else {
100 return linkValue;
101 }
102 break;
103 case 528:
104 if ( PROPERTIES[ numericPropertyId ].hasOwnProperty( catalogMap[ linkValue ] ) ) {
105 linkTemplate = PROPERTIES[ numericPropertyId ][ catalogMap[ linkValue ] ];
106 } else {
107 return linkValue;
108 }
109 break;
110 case 791:
111 if ( linkValue.substring( 0, 3 ) === 'DE-' ) {
112 linkTemplate = 'http://dispatch.opac.d-nb.de/DB=1.2/CMD?ACT=SRCHA&IKT=8529&TRM=$1';
113 } else {
114 return linkValue;
115 }
116 break;
117 }
118
119 return $( '<a>' )
120 .text( displayText )
121 .attr( 'href', linkTemplate.replace( /\$1/g, linkValue ) )
122 // Show the 'external link' icon:
123 .addClass( 'external' );
124}
125
126function createLinkForString( numericPropertyId, value ) {
127 var linkValue = getLinkValueForString( numericPropertyId, value );
128 return makeLink( numericPropertyId, linkValue, value );
129}
130
131function createLinkForSnakValue( numericPropertyId, dataValue, displayText ) {
132 var dataValueType = dataValue.getType(),
133 value = dataValue.toJSON();
134
135 // @fixme shouldn't happen but in case of any unexpected data value types,
136 // then there should be better error handling here.
137 var linkValue = '';
138
139 if ( dataValueType === 'string' ) {
140 linkValue = getLinkValueForString( numericPropertyId, value );
141 } else if ( dataValueType === 'globecoordinate' ) {
142 linkValue = getGeoHackParams( value );
143 }
144
145 return makeLink( numericPropertyId, linkValue, displayText );
146}
147
148function linkSnakView( el, propertySelector, valueSelector ) {
149 var $propLink = $( el ).find( propertySelector );
150
151 var title = $propLink.attr( 'title' );
152
153 if ( title ) {
154 var titleParts = title.split( ':P' ),
155 numericPropertyId = titleParts[ 1 ];
156
157 if ( PROPERTIES.hasOwnProperty( numericPropertyId ) ) {
158 var $value = $( el ).find( valueSelector ).first(),
159 $link = createLinkForString( numericPropertyId, $value.text() );
160
161 $value.html( $link );
162 }
163 }
164}
165
166function handleSnak( snak, snakView ) {
167 if ( !( snak instanceof wikibase.datamodel.PropertyValueSnak ) ) {
168 return;
169 }
170
171 var numericPropertyId = snak.getPropertyId().slice( 1 );
172 if ( !( PROPERTIES.hasOwnProperty( numericPropertyId ) ) ) {
173 return;
174 }
175 var $snakValue = $( snakView ).find( '.wikibase-snakview-value' ),
176 displayText = $snakValue.text(),
177 snakLink = createLinkForSnakValue( numericPropertyId, snak.getValue(), displayText );
178 $snakValue.html( snakLink );
179}
180
181/**
182 * Initializes the gadget.
183 * This procedure needs to be performed as good as possible. jQuery selector usage should be limited
184 * to a minimum.
185 */
186function initGadget() {
187 if ( $.isEmptyObject( PROPERTIES ) ) {
188 return;
189 }
190
191 $( ':wikibase-statementview' ).each( function () {
192 var statementview = $.data( this, 'statementview' ),
193 statement = statementview.value(),
194 claim = statement.getClaim(),
195 qualifierGroups = claim.getQualifiers().getGroupedSnakLists();
196
197 handleSnak( claim.getMainSnak(), statementview.$mainSnak[0] );
198
199 $( '.wikibase-statementview-qualifiers .wikibase-snaklistview', this ).each( function( i ) {
200 var qualifiers = qualifierGroups[i].toArray();
201 $( '.wikibase-snakview', this ).each( function( n ) {
202 handleSnak( qualifiers[n], this );
203 } );
204 } );
205
206 } );
207
208 $( '.wikibase-referenceview .wikibase-snaklistview-listview' ).each( function () {
209 linkSnakView( this, '.wikibase-snakview-property > a', '.wikibase-snakview-value' );
210 } );
211}
212
213function getProperties() {
214 var api = new mw.Api(),
215 repoApi = new wb.api.RepoApi( api ),
216 prop,
217 entityIds = [],
218 alreadyLinkedPropertyIds = [],
219 entity = JSON.parse( mw.config.get( 'wbEntity' ) );
220
221 function addSnak( snak ) {
222 var snakPropertyId = snak.property;
223
224 if ( snak.snaktype !== 'value' ||
225 ( snak.datavalue.type !== 'string' && snak.datavalue.type !== 'globecoordinate' ) ) {
226 return;
227 }
228 if ( entityIds.indexOf( snakPropertyId ) !== -1 ) {
229 return;
230 }
231 if ( specialHandlingProperties.indexOf( snakPropertyId ) === -1 ) {
232 if ( alreadyLinkedPropertyIds.indexOf( snakPropertyId ) !== -1 ) {
233 return;
234 }
235 if ( $( '#' + snakPropertyId ).find( '.wikibase-snakview-variation-valuesnak:first > a' ).length > 0 ) {
236 alreadyLinkedPropertyIds.push( snakPropertyId );
237 return;
238 }
239 }
240
241 entityIds.push( snakPropertyId );
242 }
243
244 for ( prop in entity.claims ) {
245 $.each( entity.claims[ prop ], function ( i, claim ) {
246 addSnak( claim.mainsnak );
247 $.each( claim.references || [], function ( i, ref ) {
248 for ( prop in ref.snaks ) {
249 $.each( ref.snaks[ prop ], function ( i, cl ) {
250 addSnak( cl );
251 } );
252 }
253 } );
254 var qprop;
255 for ( qprop in claim.qualifiers || {} ) {
256 $.each( claim.qualifiers[ qprop ], function ( i, cl ) {
257 addSnak( cl );
258 if ( qprop == 'P972' && prop == 'P528' ) {
259 var itemId = 'Q' + cl.datavalue.value['numeric-id'];
260 entityIds.push( itemId );
261 catalogMap[ claim.mainsnak.datavalue.value ] = itemId;
262 }
263 } );
264 }
265 } );
266 }
267 if ( !propertyIds.length ) {
268 return $.Deferred().resolve();
269 }
270 return repoApi.getEntities( entityIds, 'claims' )
271 .done( function ( data ) {
272 $.each( data.entities, function ( entityId, entity ) {
273 if ( entity.hasOwnProperty( 'datatype' ) &&
274 entity.datatype === "external-id" &&
275 $.inArray( entity.id, specialHandlingProperties ) === -1 ) {
276 // No need to format these
277 return true;
278 }
279 $.each( entity.claims, function ( claimId, claims ) {
280 if ( claimId === 'P1630' ) {
281 if ( entity.hasOwnProperty( 'datatype' ) ) {
282 PROPERTIES[ entityId.slice( 1 ) ] = claims[ 0 ].mainsnak.datavalue.value;
283 } else {
284 if ( !PROPERTIES.hasOwnProperty( '528' ) ) {
285 PROPERTIES[ '528' ] = {};
286 }
287 PROPERTIES[ '528' ][ entityId ] = claims[ 0 ].mainsnak.datavalue.value;
288 }
289 }
290 } );
291 } );
292 } );
293}
294
295getProperties().done( function () {
296 $( initGadget );
297} );
298
299}( mediaWiki, wikibase, jQuery ) );

I still think that such modeling needs an RfC or something along these lines, because it essentially hugely complicates things.