User:EpochFail/ArticleQuality-system.js: Difference between revisions
Appearance
From Meta, a Wikimedia project coordination wiki
localize score
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(" ")) .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)