Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
OresMetadata
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 8
240
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 newFromGlobalState
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getMetadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getArticleQualityClass
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getDraftQualityClass
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 classToMessage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 fetchScores
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 getORESScores
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18namespace MediaWiki\Extension\PageTriage;
19
20use IContextSource;
21use LogicException;
22use ORES\Services\ORESServices;
23use ORES\Storage\ModelLookup;
24use ORES\Storage\ThresholdLookup;
25use Wikimedia\Rdbms\IResultWrapper;
26
27/**
28 * Helper class to add metadata to articles in the list view of Special:NewPagesFeed.
29 *
30 * @package MediaWiki\Extension\PageTriage
31 */
32class OresMetadata {
33
34    /**
35     * @var ThresholdLookup
36     */
37    private $thresholdLookup;
38
39    /**
40     * @var ModelLookup
41     */
42    private $modelLookup;
43
44    /**
45     * @var array
46     */
47    private $oresModelClasses;
48
49    /**
50     * @var IContextSource
51     */
52    private $requestContext;
53
54    /**
55     * @var array
56     */
57    private $scores;
58
59    /**
60     * @var string[] Map of ORES class names from thresholds lookup mapped to translatable strings.
61     */
62    private const ORES_CLASS_TO_MSG_KEY = [
63        'Stub' => 'pagetriage-filter-stat-predicted-class-stub',
64        'Start' => 'pagetriage-filter-stat-predicted-class-start',
65        'C' => 'pagetriage-filter-stat-predicted-class-c',
66        'B' => 'pagetriage-filter-stat-predicted-class-b',
67        'GA' => 'pagetriage-filter-stat-predicted-class-good',
68        'FA' => 'pagetriage-filter-stat-predicted-class-featured',
69        'vandalism' => 'pagetriage-filter-stat-predicted-issues-vandalism',
70        'attack' => 'pagetriage-filter-stat-predicted-issues-attack',
71        'spam' => 'pagetriage-filter-stat-predicted-issues-spam',
72        'OK' => false,
73    ];
74
75    /**
76     * OresMetadata constructor.
77     * @param ThresholdLookup $thresholdLookup
78     * @param ModelLookup $modelLookup
79     * @param array $oresModelClasses
80     * @param IContextSource $requestContext
81     * @param int[] $pageIds
82     */
83    public function __construct(
84        ThresholdLookup $thresholdLookup,
85        ModelLookup $modelLookup,
86        array $oresModelClasses,
87        IContextSource $requestContext,
88        $pageIds
89    ) {
90        $this->thresholdLookup = $thresholdLookup;
91        $this->modelLookup = $modelLookup;
92        $this->oresModelClasses = $oresModelClasses;
93        $this->requestContext = $requestContext;
94
95        // Pre-fetch the ORES scores for all the pages of interest
96        $this->scores = $this->fetchScores( $pageIds );
97    }
98
99    /**
100     * Create an instance of OresMetadata by getting dependencies from
101     * global variables and static ORESServices
102     *
103     * @param IContextSource $context
104     * @param int[] $pageIds
105     * @return OresMetadata
106     */
107    public static function newFromGlobalState( IContextSource $context, $pageIds ) {
108        global $wgOresModelClasses;
109        return new self(
110            ORESServices::getThresholdLookup(),
111            ORESServices::getModelLookup(),
112            $wgOresModelClasses,
113            $context,
114            $pageIds
115        );
116    }
117
118    /**
119     * Get ORES metadata (articlequality, draftquality) from the database for an article.
120     *
121     * @param int $pageId
122     * @return array
123     *   An array to merge in with other metadata for the article.
124     */
125    public function getMetadata( $pageId ) {
126        return $this->scores[ $pageId ];
127    }
128
129    /**
130     * @param float $probability
131     * @return string Name of the class corresponding to the given probability
132     */
133    private function getArticleQualityClass( $probability ) {
134        $thresholds = $this->thresholdLookup->getThresholds( 'articlequality' );
135        foreach ( $thresholds as $className => $threshold ) {
136            if ( $probability >= $threshold[ 'min' ] &&
137                $probability <= $threshold[ 'max' ] ) {
138                return $className;
139            }
140        }
141
142        throw new LogicException( "Couldn't determine quality class for probability $probability" );
143    }
144
145    /**
146     * @param int $classId
147     * @return string Name of the class corresponding to the given class id
148     */
149    private function getDraftQualityClass( $classId ) {
150        $modelClasses = array_flip( $this->oresModelClasses[ 'draftquality' ] );
151        return $modelClasses[ $classId ];
152    }
153
154    /**
155     * @param string $className
156     * @return string Translated name of the class
157     */
158    private function classToMessage( $className ) {
159        $key = self::ORES_CLASS_TO_MSG_KEY[ $className ];
160        return $key ? $this->requestContext->msg( $key )->text() : '';
161    }
162
163    /**
164     * Fetch the 'articlequality' and 'draftquality' scores for the given page ids
165     *
166     * @param int[] $pageIds
167     * @return array
168     */
169    private function fetchScores( $pageIds ) {
170        $pendingScore = $this->requestContext->msg(
171            'pagetriage-filter-pending-ores-score' )->text();
172
173        $scores = [];
174        foreach ( $pageIds as $pageId ) {
175            $scores[ $pageId ] = [
176                'ores_articlequality' => $pendingScore,
177                'ores_draftquality' => '',
178            ];
179        }
180
181        $result = $this->getORESScores( 'articlequality', $pageIds );
182        foreach ( $result as $row ) {
183            $scores[$row->ptrp_page_id]['ores_articlequality'] = $this->classToMessage(
184                $this->getArticleQualityClass( $row->oresc_probability ) );
185        }
186
187        $result = $this->getORESScores( 'draftquality', $pageIds, [ 'oresc_is_predicted' => 1 ] );
188        foreach ( $result as $row ) {
189            $scores[$row->ptrp_page_id]['ores_draftquality'] = $this->classToMessage(
190                $this->getDraftQualityClass( $row->oresc_class ) );
191        }
192
193        return $scores;
194    }
195
196    /**
197     * Select ORES scores from the database.
198     *
199     * @param string $modelName
200     * @param int[] $pageIds
201     * @param array $extraConds
202     * @return IResultWrapper
203     */
204    private function getORESScores( $modelName, $pageIds, $extraConds = [] ) {
205        $dbr = PageTriageUtil::getReplicaConnection();
206        $result = $dbr->newSelectQueryBuilder()
207            ->select( [
208                'ptrp_page_id',
209                // used for articlequality
210                'oresc_probability',
211                // used for draftquality
212                'oresc_class',
213            ] )
214            ->from( 'pagetriage_page' )
215            ->join( 'page', 'page', 'ptrp_page_id=page_id' )
216            ->leftJoin( 'ores_classification', 'ores_classification', 'page_latest=oresc_rev' )
217            ->where( [
218                'oresc_model' => $this->modelLookup->getModelId( $modelName ),
219                'ptrp_page_id' => $pageIds,
220            ] + $extraConds )
221            ->caller( __METHOD__ )
222            ->fetchResultSet();
223
224        return $result;
225    }
226}