User:Frettie/consistency check add.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
if ( typeof ( $ ) === 'undefined' ) $ = jQuery;

var consistency_check = {

	_entity: null,

	_refresh: false,

	running: false,

	reciproke: {
		'P26': ['P26'], // spouse
		'P451': ['P451'], // partner
		'P40': ['P22', 'P25'], // mother, father
		'P3373': ['P3373'], // brother and sister
		'P301': ['P910'], // main article
		'P1629': ['P1687'], // subject item of this property / Wikidata property
		'P2875': ['P1659'], // property usage tracking category / see also
		'P1753': ['P1754'], // related list
		'P361': ['P527'], // parts
		'P461': ['P461'], // opposite of
		'P460': ['P460'], // said to be same as
		'P1889': ['P1889'], // different from
		'P2959': ['P2959'], // permanent duplicated
		'P710': ['P1344'], // participant (of)
		'P197': ['P197'], // adjacent station
		'P2789': ['P2789'], // connects with
		'P403': ['P974'], // mouth of the watercourse / tributary
		'P155': ['P156'], // previous / next
		'P1365': ['P1366'], // replaced by /replaces
		'P167': ['P1398'], // structure replaced by / replaces
		'P47': ['P47'], // shares border with
		'P190': ['P190'], // twinned administrative body
		'P36': ['P1376'], // capital (of)
		'P530': ['P530'], // diplomatic relation
		'P828': ['P1542'], // cause
		'P802': ['P1066'],
		'P749': ['P355'],
		'P747': ['P629'],
		'P674': ['P1441'], // charaters and present in work
		'P1830': ['P127'], // owner of/owned by
		'P1382': ['P1382'], // partially coincident with
		'P1560': ['P1560'], // given name version for other gender
		'P2743': ['P2743'], // this zoological name is coordinate with
		'P184': ['P185'], // doctoral advisor
		'P2674': ['P2673'], // next crossing downstream
		'P3032': ['P3032'], // adjacent building
		'P1423': ['P1424'], // template has topic / topic's main template
		'P2388': ['P2389'], // organization directed by the office or position
		'P4969': ['P144'], //  opera derivata (P4969) <-> basato su (P144)
		
	},

	init: function ( entity ) {
		var self = this;
		if ( ( mw.config.get( 'wgNamespaceNumber' ) !== 0 && mw.config.get( 'wgNamespaceNumber' ) !== 120 ) ||
			!mw.config.exists( 'wbEntityId' ) ||
			!mw.config.get( 'wbIsEditView' )
		) {
			return;
		}

		self._entity = entity;
		self.entityId = mw.config.get( 'wbEntityId' );
		if(!wb || !wb.api || !wb.api.RepoApi) return; // TypeError: Cannot read property 'RepoApi' of undefined (e.g. mobile site)
		self.repoApi = new wb.api.RepoApi( new mw.Api() );

		$.each( self.reciproke, function ( key, props ) {
			props.forEach( function ( prop ) {
				if ( undefined === self.reciproke[prop] ) self.reciproke[prop] = [];
				if ( self.reciproke[prop].indexOf( key ) === -1 ) self.reciproke[prop].push( key );
			} );
		} );

		mw.hook( 'wikibase.statement.saved' ).add( function ( _, statement ) {
			self._entity = null;
			self._refresh = true;
			// FIXME: buggy, disabled per talk page and T252078
			// self.reciprocityAdder( statement );
		} );

		var portletLink = mw.util.addPortletLink( 'p-tb', '#', 'Consistency', 't-consistency_check' );
		$( portletLink ).on( 'click', function ( e ) {
			e.preventDefault();
			self.run();
			return false;
		} );
	},

	getEntity: function () {
		var self = this;
		if ( self._entity !== null ) {
			return $.Deferred().resolve( self._entity );
		} else {
			return self.repoApi.getEntities( [ self.entityId ] )
			.then( function ( data ) {
				self._entity = data.entities[ self.entityId ];
				return self._entity;
			} );
		}
	},

	run: function () {
		var self = this;
		if ( self.running ) return;
		self.running = true;
		var consistency_section = $( '#consistency-section' );
		if ( consistency_section.length !== 0 && self._refresh !== true ) {
			self.running = false;
			return;
		}

		consistency_section.remove();
		self._refresh = false;

		var langs = mw.language.getFallbackLanguageChain();

		self.getEntity()
		.then( function ( entity ) {
			var check = {},
				props = [],
				propLabels = {};
			$.each( entity.claims || {}, function ( property, claims ) {
				if ( undefined === self.reciproke[ property ] ) return; // No need to check

				var load_property = false;
				claims.forEach( function ( claim ) {
					if ( !claim.mainsnak.datavalue ) return;
					const q = claim.mainsnak.datavalue.value.id;
					if ( check[ q ] === undefined ) {
						check[ q ] = [];
					}
					check[ q ].push( {
						id: 'consistency_' + property + '_' + q,
						prop: property,
					} );
					load_property = true;
				} );
				if ( load_property ) {
					props.push( property );
				}
			} );

			var promise;
			if ( props.length > 0 ) {
				promise = self.repoApi.getEntities(
					props,
					[ 'labels' ],
					langs
				).then( function( data ) {
					props.forEach( function ( p ) {
						propLabels[ p ] = p;
						var labels = data.entities[ p ].labels;
						$.each( langs, function ( _, lang ) {
							if ( labels[ lang ] !== undefined ) {
								propLabels[ p ] = labels[ lang ].value;
								return false;
							}
						} );
					} );
					var table = '<div><table border=1>';
					$.each( check, function ( q, objs ) {
						objs.forEach( function ( d ) {
							table += '<tr><th class="label_' + d.prop + '">' + propLabels[ d.prop ] + '</th>';
							table += '<td id="' + d.id + '_c" style="text-align:center">?</td>';
							table += '<td id="' + d.id + '_v"><a id="' + d.id + '_vh" href="/entity/' + q + '">' + q + '</a></td></tr>';
						} );
					} );
					table += "</table></div>";
					return table;
				} );
			} else {
				promise = $.Deferred().resolve( '<div>No claims found for ' + self.entityId + '.</div>' ).promise();
			}

			return promise.then( function ( table ) {
				const header = '<h2 class="wb-section-heading" id="consistency-heading">Consistency</h2>';

				$( '<div>' )
				.attr( 'id', 'consistency-section' )
				.addClass( 'consistency' )
				.html( header + table )
				.insertBefore( $( 'h2.wb-section-heading' ).get( 0 ) );
	
				if ( check.length === 0 ) {
					return;
				}

				const
					oneway = '<span class="oneway" style="color:red; font-weight:bold">&rarr;</span>',
					bothways = '<span style="color:green">&harr;</span>',
					append = '&nbsp;<span class="append" style="cursor:pointer; color:red; font-weight:bold">&#43;</span>';

				var ids = Object.keys( check ),
					idgroups = [];
				const
					length = ids.length,
					chunk = 40;

				for (var i = 0; i < length; i += chunk) {
					idgroups.push(ids.slice(i, i+chunk));
				}

				return $.when.apply( $, idgroups.map( function ( group ) {
					return self.repoApi.getEntities(
						group,
						[ 'claims', 'info', 'labels' ],
						langs
					)
					.then( function ( data ) {
						$.each( data.entities, function ( id, entity ) {
							var labelTo = '';
							$.each( langs, function ( _, lang ) {
								if ( entity.labels[lang] !== undefined ) {
									labelTo = entity.labels[lang].value;
									return false;
								}
							} );
							check[ id ].forEach( function ( d ) {
								var found = false,
									has = [];
								$.each( self.reciproke[ d.prop ], function ( _, prop ) {
									$.each( entity.claims[ prop ] || [], function ( _, value ) {
										if ( !value.mainsnak.datavalue ) return;
										if ( has.indexOf( prop ) === -1 ) {
											has.push( prop );
										}
										const q = value.mainsnak.datavalue.value.id;
										found = ( q === self.entityId );
										return !found;
									} );
									return !found;
								} );

								if ( labelTo !== '' ) {
									$( '#' + d.id + '_vh' ).text( labelTo );
								}

								var $click = $( '#' + d.id + '_c' );
								if ( found ) {
									$click.html( bothways );
								} else if (
									( self.reciproke[ d.prop ].length === 1 ) ||
									( self.reciproke[ d.prop ].length - has.length === 1 )
								) {
									var prop;
									$.each( self.reciproke[ d.prop ], function ( _, _prop ) {
										prop = _prop;
										return has.indexOf( _prop ) !== -1;
									} );
									$click
									.html( oneway + append )
									.children( 'span.append' )
									.data( 'prop', prop )
									.data( 'target', id )
									.data( 'lastrevid', entity.lastrevid )
									.on( 'click', function ( event ) {
										event.preventDefault();
										self.addStatement( this );
									} );
								}
							} );
						} );
					} );
				} ) );
			} )
			.then( function () {
				self.running = false;
			} );
		} );
	},

	createClaim: function ( entityId, baseRevId, snakType, propertyId, value ) {
		var self = this;
		if (
			typeof entityId !== 'string'
			|| typeof baseRevId !== 'number'
			|| typeof snakType !== 'string'
			|| typeof propertyId !== 'string'
			|| value && typeof value !== 'string' && typeof value !== 'object'
		) {
			throw new Error( 'Parameter not specified properly' );
		}

		var params = {
			action: 'wbcreateclaim',
			entity: entityId,
			baserevid: baseRevId,
			snaktype: snakType,
			property: propertyId,
			summary: 'using [[User:Frettie/consistency check add.js]]',
		};

		if ( value ) {
			params.value = JSON.stringify( value );
		}

		return self.repoApi.post( params );
	},

	addStatement: function ( el ) {
		var $this = $( el ),
			self = this;

		self.createClaim(
			$this.data( 'target' ),
			$this.data( 'lastrevid' ),
			'value',
			$this.data( 'prop' ),
			{ id: self.entityId }
		)
		.then( function ( data ) {
			var color;
			if ( data.success ) {
				$this
					.html( '&harr;' )
					.css( 'color', 'green' )
					.siblings( 'span' ).remove();
				color = '#baffc9';
			} else {
				color = '#ffb3ba';
			}
			$this.parent().parent().css( 'background-color', color );
		} );
	},

	reciprocityAdder: function ( statement ) {
		var self = this;

		self.repoApi._api.get( {
			action: 'wbgetclaims',
			claim: statement
		} )
		.done( function ( data ) {
			var target = null, prop = null;
			$.each( data.claims, function ( _prop, value ) {
				prop = _prop;
				if ( value[0].mainsnak.datavalue ) {
					target = value[0].mainsnak.datavalue.value.id;
				}
				return false;
			} );
			if ( !prop || !target || ( self.reciproke[ prop ] || [] ).length !== 1 ) {
				return;
			}

			self.repoApi.getEntities(
				[ target ],
				[ 'claims', 'info' ]
			)
			.done( function ( data ) {
				var found = false;
				$.each( data.entities[ target ].claims[ prop ] || [], function ( _, value ) {
					if ( !value.mainsnak.datavalue ) return;
					const q = value.mainsnak.datavalue.value.id;
					found = ( q === self.entityId );
					return !found;
				} );

				if ( found ) {
					return;
				}

				self.createClaim(
					target,
					data.entities[ target ].lastrevid,
					'value',
					self.reciproke[ prop ][ 0 ],
					{ id: self.entityId }
				)
				.done( function ( data ) {
					// TODO: use mw.notify?
					var color = data.success ? '#baffc9' : '#ffb3ba',
						selector = $.escapeSelector( statement );
					$( '#' + selector ).css( 'background-color', color );
				} );
			} );
		} );
	},

	the_end: ''
};

$( function () {
	mw.hook( 'wikibase.entityPage.entityLoaded' ).add( function ( entity ) {
		consistency_check.init( entity );
	} );
} );