Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 54
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 / 54
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 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newFromGlobalState
0.00% covered (danger)
0.00%
0 / 8
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 LogicException;
21use MediaWiki\Context\IContextSource;
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 array
36     */
37    private $scores;
38
39    /**
40     * @var string[] Map of ORES class names from thresholds lookup mapped to translatable strings.
41     */
42    private const ORES_CLASS_TO_MSG_KEY = [
43        'Stub' => 'pagetriage-filter-stat-predicted-class-stub',
44        'Start' => 'pagetriage-filter-stat-predicted-class-start',
45        'C' => 'pagetriage-filter-stat-predicted-class-c',
46        'B' => 'pagetriage-filter-stat-predicted-class-b',
47        'GA' => 'pagetriage-filter-stat-predicted-class-good',
48        'FA' => 'pagetriage-filter-stat-predicted-class-featured',
49        'vandalism' => 'pagetriage-filter-stat-predicted-issues-vandalism',
50        'attack' => 'pagetriage-filter-stat-predicted-issues-attack',
51        'spam' => 'pagetriage-filter-stat-predicted-issues-spam',
52        'OK' => false,
53    ];
54
55    /**
56     * OresMetadata constructor.
57     * @param ThresholdLookup $thresholdLookup
58     * @param ModelLookup $modelLookup
59     * @param array $oresModelClasses
60     * @param IContextSource $requestContext
61     * @param int[] $pageIds
62     */
63    public function __construct(
64        private readonly ThresholdLookup $thresholdLookup,
65        private readonly ModelLookup $modelLookup,
66        private readonly array $oresModelClasses,
67        private readonly IContextSource $requestContext,
68        $pageIds
69    ) {
70        // Pre-fetch the ORES scores for all the pages of interest
71        $this->scores = $this->fetchScores( $pageIds );
72    }
73
74    /**
75     * Create an instance of OresMetadata by getting dependencies from
76     * global variables and static ORESServices
77     *
78     * @param IContextSource $context
79     * @param int[] $pageIds
80     * @return OresMetadata
81     */
82    public static function newFromGlobalState( IContextSource $context, $pageIds ) {
83        $config = $context->getConfig();
84        return new self(
85            ORESServices::getThresholdLookup(),
86            ORESServices::getModelLookup(),
87            $config->get( 'OresModelClasses' ),
88            $context,
89            $pageIds
90        );
91    }
92
93    /**
94     * Get ORES metadata (articlequality, draftquality) from the database for an article.
95     *
96     * @param int $pageId
97     * @return array
98     *   An array to merge in with other metadata for the article.
99     */
100    public function getMetadata( $pageId ) {
101        return $this->scores[ $pageId ];
102    }
103
104    /**
105     * @param float $probability
106     * @return string Name of the class corresponding to the given probability
107     */
108    private function getArticleQualityClass( $probability ) {
109        $thresholds = $this->thresholdLookup->getThresholds( 'articlequality' );
110        foreach ( $thresholds as $className => $threshold ) {
111            if ( $probability >= $threshold[ 'min' ] &&
112                $probability <= $threshold[ 'max' ] ) {
113                return $className;
114            }
115        }
116
117        throw new LogicException( "Couldn't determine quality class for probability $probability" );
118    }
119
120    /**
121     * @param int $classId
122     * @return string Name of the class corresponding to the given class id
123     */
124    private function getDraftQualityClass( $classId ) {
125        $modelClasses = array_flip( $this->oresModelClasses[ 'draftquality' ] );
126        return $modelClasses[ $classId ];
127    }
128
129    /**
130     * @param string $className
131     * @return string Translated name of the class
132     */
133    private function classToMessage( $className ) {
134        $key = self::ORES_CLASS_TO_MSG_KEY[ $className ];
135        return $key ? $this->requestContext->msg( $key )->text() : '';
136    }
137
138    /**
139     * Fetch the 'articlequality' and 'draftquality' scores for the given page ids
140     *
141     * @param int[] $pageIds
142     * @return array
143     */
144    private function fetchScores( $pageIds ) {
145        $pendingScore = $this->requestContext->msg(
146            'pagetriage-filter-pending-ores-score' )->text();
147
148        $scores = [];
149        foreach ( $pageIds as $pageId ) {
150            $scores[ $pageId ] = [
151                'ores_articlequality' => $pendingScore,
152                'ores_draftquality' => '',
153            ];
154        }
155
156        $result = $this->getORESScores( 'articlequality', $pageIds );
157        foreach ( $result as $row ) {
158            $scores[$row->ptrp_page_id]['ores_articlequality'] = $this->classToMessage(
159                $this->getArticleQualityClass( $row->oresc_probability ) );
160        }
161
162        $result = $this->getORESScores( 'draftquality', $pageIds, [ 'oresc_is_predicted' => 1 ] );
163        foreach ( $result as $row ) {
164            $scores[$row->ptrp_page_id]['ores_draftquality'] = $this->classToMessage(
165                $this->getDraftQualityClass( $row->oresc_class ) );
166        }
167
168        return $scores;
169    }
170
171    /**
172     * Select ORES scores from the database.
173     *
174     * @param string $modelName
175     * @param int[] $pageIds
176     * @param array $extraConds
177     * @return IResultWrapper
178     */
179    private function getORESScores( $modelName, $pageIds, $extraConds = [] ) {
180        $dbr = PageTriageUtil::getReplicaConnection();
181        $result = $dbr->newSelectQueryBuilder()
182            ->select( [
183                'ptrp_page_id',
184                // used for articlequality
185                'oresc_probability',
186                // used for draftquality
187                'oresc_class',
188            ] )
189            ->from( 'pagetriage_page' )
190            ->join( 'page', 'page', 'ptrp_page_id=page_id' )
191            ->leftJoin( 'ores_classification', 'ores_classification', 'page_latest=oresc_rev' )
192            ->where( [
193                'oresc_model' => $this->modelLookup->getModelId( $modelName ),
194                'ptrp_page_id' => $pageIds,
195            ] + $extraConds )
196            ->caller( __METHOD__ )
197            ->fetchResultSet();
198
199        return $result;
200    }
201}