Jump to content
Wikimedia Meta-Wiki

User:EpochFail/ArticleQuality-system.js: Difference between revisions

From Meta, a Wikimedia project coordination wiki
Content deleted Content added
localize score
Jon (WMF) (talk | contribs)
338 edits
maintenance: more info TypeError: Cannot read property '0' of null, seen in high volume at eu.wikipedia.org and possible when wgULSAcceptLanguageList is not set in accept headers
Line 321: Line 321:
var roundedWeightedSum = Math.round(weightedSum*100)/100;
var roundedWeightedSum = Math.round(weightedSum*100)/100;
var localizedWeightedSum = roundedWeightedSum.toLocaleString(
var localizedWeightedSum = roundedWeightedSum.toLocaleString(
mw.config.get("wgULSAcceptLanguageList")[0])
mw.config.get("wgULSAcceptLanguageList"(追記) , window.navigator.languages || [] (追記ここまで))[0])
return this.assessment_system + ": " +
return this.assessment_system + ": " +

Revision as of 05:03, 26 October 2021

 (function($,mw){
 mw.loader.using(['mediawiki.api']).done(function(){
 vardefaultOptions={
 parameters:{
 format:'json'
 },
 ajax:{
 timeout:30*1000,// 30 seconds
 dataType:'json',
 type:'GET'
 },
 score:{
 batchSize:50,
 maxWorkers:4
 }
 };

 /**
 		 * Constructor to create a pool of workers to request a set of model-scores
 		 * from the ORES api. This object will allow scoring jobs to be
 		 * transparently started within a worker pool. The worker pool will then
 		 * manage all requests in order to comply with the options provided
 		 * (e.g. batchSize and maxWorkers).
 		 *
 		 * var ores = require('ext.ores.api');
 		 * aqPool = ores.Pool([ "articlequality" ], {batchSize: 50, maxWorkers: 3})
 		 *
 		 * aqPool.score(214412)
 		 * .done(function(scoreDoc){...})
 		 * aqPool.score(214413)
 		 * .done(function(scoreDoc){...})
 		 *
 		 * @constructor
 		 * @param {Object} [oresApi] An OresApi object to use for querying
 		 * @param {Array} [models] A list of models to query for in each request
 		 * @param {Object} [options] A set of options for querying. See defaultOptions.score above
 		 */
 varOresScoreBatcherPool=function(oresApi,models,options){
 this.oresApi=oresApi;
 this.models=models;
 this.batchSize=options.batchSize||defaultOptions.score.batchSize;
 this.maxWorkers=options.maxWorkers||defaultOptions.score.maxWorkers;
 this.liveWorkers=0;
 this.taskQueue=[];
 this.tasksQueued=$.Deferred()
 .progress(this.ensureWorkers.bind(this));
 };
 OresScoreBatcherPool.prototype={
 /**
 			 * Add a score job to the queue and return a promise for the result.
 			 *
 			 * @param {number} [revId] A revision to score with the given model
 			 */
 score:function(revId){
 varresultDfd=$.Deferred(),
 task={revId:revId,resultDfd:resultDfd};

 this.taskQueue.push(task);
 this.tasksQueued.notify();
 returnresultDfd.promise();
 },
 /**
 			 * Process a batch and recurse until the taskQueue is empty
 			 */
 processTaskBatches:function(){
 // Get a batch to process
 batch=this.taskQueue.splice(0,this.batchSize);

 // If there's stuff to process, start a new score batch processing job and
 // recurse when it finishes.
 if(batch.length){
 this.scoreBatch(batch)
 .fail(function(){
 for(vari=0;i<batch.length;i++){
 batch[i].resultDfd.error.apply(null,arguments);
 }
 })
 .always(this.processTaskBatches.bind(this));
 }else{
 // shut down worker and decrement the worker count
 this.liveWorkers-=1;
 console.debug("Shutting down worker");
 }
 },
 /**
 			 * Generate a set of scores for a batch. Results will be sent to specific
 			 * deferred result objects.
 			 */
 scoreBatch:function(batch){
 varbatchDfd=$.Deferred(),
 revIds=[];

 for(vari=0;i<batch.length;i++){
 revIds.push(batch[i].revId);
 }

 this.oresApi.get({revids:revIds,models:this.models})
 .done(function(responseDoc){
 for(vari=0;i<batch.length;i++){
 varscoreDoc=responseDoc[this.oresApi.options.dbname].scores[batch[i].revId];
 batch[i].resultDfd.resolve(scoreDoc);
 }
 }.bind(this))
 .fail(function(){
 for(vari=0;i<batch.length;i++){
 batch[i].resultDfd.error.apply(null,arguments);
 }
 })
 .always(function(){batchDfd.resolve()});

 returnbatchDfd.promise();
 },
 /**
 			 * Ensure that workers are running. This method is used when a task is
 			 * added to the queue to make sure that the workers are started/restarted
 			 * if necessary. Note that a 50ms delay is implemented for starting a
 			 * worker process to ensure that tasks are given enough time to enqueue
 			 * before batch processing starts.
 			 */
 ensureWorkers:function(){
 while(this.liveWorkers<this.maxWorkers){
 console.debug("Starting up worker");
 setTimeout(
 function(){this.processTaskBatches()}.bind(this),
 50);
 this.liveWorkers+=1;
 }
 }
 };

 /**
 		 * Constructor to create an object to interact with the API of an ORES server.
 		 * OresApi objects represent the API of a particular ORES server.
 		 *
 		 * var ores = require('ext.ores.api');
 		 * ores.get( {
 		 * revids: [1234, 1235],
 		 * models: [ 'damaging', 'articlequality' ] // same effect as 'damaging|articlequality'
 		 * } ).done( function ( data ) {
 		 * console.log( data );
 		 * } );
 		 *
 		 * @constructor
 		 * @param {Object} [options] See #defaultOptions documentation above.
 		 */
 varOresApi=function(options){

 options.parameters=$.extend({},defaultOptions.parameters,options.parameters);
 options.ajax=$.extend({},defaultOptions.ajax,options.ajax);
 options.score=$.extend({},defaultOptions.score,options.score);

 if(options.ajax.url){
 options.ajax.url=String(options.ajax.url);
 }else{
 options.ajax.url=options.host+'/v3/scores/'+options.dbname;
 }

 this.options=options;
 this.requests=[];
 };

 OresApi.prototype={
 /**
 			 * Abort all unfinished requests issued by this Api object.
 			 *
 			 * @method
 			 */
 abort:function(){
 this.requests.forEach(function(request){
 if(request){
 request.abort();
 }
 });
 },

 /**
 			 * Massage parameters from the nice format we accept into a format suitable for the API.
 			 *
 			 * @private
 			 * @param {Object} parameters (modified in-place)
 			 */
 preprocessParameters:function(parameters){
 varkey;
 for(keyinparameters){
 if(Array.isArray(parameters[key])){
 parameters[key]=parameters[key].join('|');
 }elseif(parameters[key]===false||parameters[key]===undefined){
 // Boolean values are only false when not given at all
 deleteparameters[key];
 }
 }
 },

 /**
 			 * Perform an API call.
 			 *
 			 * @param {Object} parameters
 			 * @return {jQuery.Promise} Done: API response data and the jqXHR object.
 			 * Fail: Error code
 			 */
 get:function(parameters){
 varrequestIndex,
 api=this,
 apiDeferred=$.Deferred(),
 xhr,
 ajaxOptions;

 parameters=$.extend({},this.options.parameters,parameters);
 ajaxOptions=$.extend({},this.options.ajax);

 this.preprocessParameters(parameters);

 ajaxOptions.data=$.param(parameters);

 xhr=$.ajax(ajaxOptions)
 .done(function(result,textStatus,jqXHR){
 varcode;
 if(result.error){
 code=result.error.code===undefined?'unknown':result.error.code;
 apiDeferred.reject(code,result,jqXHR);
 }else{
 apiDeferred.resolve(result,jqXHR);
 }
 });

 requestIndex=this.requests.length;
 this.requests.push(xhr);
 xhr.always(function(){
 api.requests[requestIndex]=null;
 });
 returnapiDeferred.promise({abort:xhr.abort}).fail(function(code,details){
 if(!(code==='http'&&details&&details.textStatus==='abort')){
 mw.log('OresApi error: ',code,details);
 }
 });
 },
 /**
 			 * Create a new OresScoreBatcherPool using this api session.
 			 *
 			 * @param {Array} [models] A list of models to query for in each request
 			 * @param {Object} [options] A set of options for querying. See defaultOptions.score above
 			 */
 pool:function(models,options){
 returnnewOresScoreBatcherPool(this,models,options);
 }
 };

 varArticleQuality=function(options){
 this.weights=options.weights;
 this.names=options.names;
 this.assessment_system=options.assessment_system;
 this.modelName=options.modelName||"articlequality";

 this.mwApi=newmw.Api();
 this.oresApi=newOresApi({host:options.ores_host,dbname:options.dbname});
 this.aqPool=this.oresApi.pool(this.modelName,{batchSize:10});
 };
 ArticleQuality.prototype={
 computeWeightedSum:function(score){
 varclsProba=score.probability;
 varweightedSum=0;
 for(varclsinclsProba){
 if(clsProba.hasOwnProperty(cls)){
 varproba=clsProba[cls];
 weightedSum+=proba*this.weights[cls];
 }
 }
 returnweightedSum;
 },
 computeWeightedProportion:function(score){
 varweightedSum=this.computeWeightedSum(score);
 returnweightedSum/Math.max.apply(null,Object.values(this.weights));
 },
 extractPrediction:function(score){
 returnscore.prediction;
 },
 parseText:function(text){
 vardfd=jQuery.Deferred();
 this.mwApi.get({action:"parse",text:text,contentmodel:"wikitext",formatversion:2,prop:"text",disablelimitreport:true})
 .done(function(data){dfd.resolve($(data.parse.text).find('p').html())})
 .fail(function(error){dfd.reject(error)});
 returndfd.promise();
 },
 getCurrentRevId:function(title){
 vardfd=jQuery.Deferred();
 this.mwApi.get({action:'query',prop:'revisions',titles:title,rvprop:'ids',formatversion:2})
 .done(function(data){
 if(data.query.pages[0].missing){
 dfd.reject('Missing page: '+data.query.pages[0].title);
 }else{
 dfd.resolve(data.query.pages[0].revisions[0].revid);
 }
 })
 .fail(function(error){dfd.reject(error)});
 returndfd.promise();
 },
 oresScore:function(revId){
 vardfd=$.Deferred();
 this.aqPool.score(revId)
 .done(function(scoreDoc){dfd.resolve(scoreDoc[this.modelName].score)}.bind(this))
 .fail(function(){dfd.error.apply(null,arguments)});
 returndfd.promise();
 },
 getAndRenderScoreHeader:function(){
 varrevId=mw.config.get('wgRevisionId');
 this.oresScore(revId)
 .done(this.renderScoreHeader.bind(this))
 .fail(function(error){console.error(error)});
 },
 renderScoreHeader:function(score){
 varrawText=this.formatScoreHeader(score);
 varqualityBlock=$('<div>').addClass("article_quality");
 $('#bodyContent').prepend(qualityBlock);
 this.parseText(rawText)
 .done(function(html){qualityBlock.html(html)})
 .fail(function(error){console.error(error)});
 },
 formatScoreHeader:function(score){
 varprediction=this.extractPrediction(score);
 varweightedSum=this.computeWeightedSum(score);
 varroundedWeightedSum=Math.round(weightedSum*100)/100;
 varlocalizedWeightedSum=roundedWeightedSum.toLocaleString(
 mw.config.get("wgULSAcceptLanguageList",window.navigator.languages||[])[0])

 returnthis.assessment_system+": "+
 this.names[prediction]+" ("+
 localizedWeightedSum+")";
 },
 renderScoreLink:function(score,span){
 varprediction=this.extractPrediction(score);
 this.parseText(this.names[prediction])
 .done(function(html){span.prepend(html)})
 .fail(function(error){console.error(error)});
 },
 getAndRenderScoreLink:function(revId,span){
 this.oresScore(revId)
 .done(function(score){this.renderScoreLink(score,span)}.bind(this))
 .fail(function(error){console.error(error)});
 },
 addScoresToArticleLinks:function(){
 $("span.ores-wp10-prediction, span.ores-quality-prediction").each(function(i,element){
 varspan=$(element);
 varanchor=span.find('a');
 varpageTitle=anchor.attr('title');
 this.getCurrentRevId(pageTitle)
 .done(function(revId){this.getAndRenderScoreLink(revId,span)}.bind(this))
 .fail(function(error){console.error(error)});
 }.bind(this));
 },
 renderHistoryScore:function(li,score){
 varlevel=Math.round(this.computeWeightedSum(score));
 varweightedProportion=this.computeWeightedProportion(score);
 varqualityPredictionNode=$("<div>").addClass("qualityprediction")
 .addClass("level_"+level)
 .append($("<div>").addClass("bar").css("width",Math.round(weightedProportion*100)+"%").append("&nbsp;"))
 .attr('title',this.formatScoreHeader(score));
 li.prepend(qualityPredictionNode);
 },
 getAndRenderHistoryScore:function(li){
 varrevId=li.attr('data-mw-revid');
 this.oresScore(revId)
 .done(function(score){this.renderHistoryScore(li,score)}.bind(this))
 .fail(function(error){console.error(error)});
 },
 getAndRenderHistoryScores:function(){
 varrevisionNodes=$('#pagehistory li');
 revisionNodes.each(function(i,element){this.getAndRenderHistoryScore($(element))}.bind(this));
 }
 }
 window.ArticleQuality=ArticleQuality;
 })
 })(jQuery,mediaWiki)

AltStyle によって変換されたページ (->オリジナル) /