Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 217
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
SuggestionListManager
0.00% covered (danger)
0.00%
0 / 217
0.00% covered (danger)
0.00%
0 / 15
992
0.00% covered (danger)
0.00%
0 / 1
 insertList
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 deleteList
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 removeTitles
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getListByConds
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getListByName
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getListById
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getDiscardedSuggestions
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getFavoriteSuggestions
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getSuggestionsByListName
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 addSuggestions
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 removeSuggestions
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 doesSuggestionExist
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 getPublicSuggestions
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getSuggestionsByType
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
 getSuggestionsInList
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace ContentTranslation;
4
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Title\Title;
7use Wikimedia\Rdbms\SelectQueryBuilder;
8
9class SuggestionListManager {
10    /**
11     * @param SuggestionList $list
12     * @return int Id of the list.
13     */
14    public function insertList( SuggestionList $list ) {
15        /** @var LoadBalancer $lb */
16        $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' );
17        $dbw = $lb->getConnection( DB_PRIMARY );
18        $values = [
19            'cxl_id' => $list->getId(),
20            'cxl_owner' => $list->getOwner(),
21            'cxl_public' => (int)$list->isPublic(),
22            'cxl_name' => $list->getName(),
23            'cxl_info' => $list->getInfo(),
24            'cxl_type' => $list->getType(),
25        ];
26
27        if ( $list->getStartTime() !== null ) {
28            $values['cxl_start_time'] = $dbw->timestamp( $list->getStartTime() );
29        }
30
31        if ( $list->getEndTime() !== null ) {
32            $values['cxl_end_time'] = $dbw->timestamp( $list->getEndTime() );
33        }
34
35        $dbw->newInsertQueryBuilder()
36            ->insertInto( 'cx_lists' )
37            ->row( $values )
38            ->caller( __METHOD__ )
39            ->execute();
40
41        return $dbw->insertId();
42    }
43
44    /**
45     * @param int $id
46     */
47    public function deleteList( $id ) {
48        /** @var LoadBalancer $lb */
49        $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' );
50        $dbw = $lb->getConnection( DB_PRIMARY );
51        $dbw->newDeleteQueryBuilder()
52            ->deleteFrom( 'cx_suggestions' )
53            ->where( [
54                'cxs_list_id' => $id,
55            ] )
56            ->caller( __METHOD__ )
57            ->execute();
58        $dbw->newDeleteQueryBuilder()
59            ->deleteFrom( 'cx_lists' )
60            ->where( [
61                'cxl_id' => $id
62            ] )
63            ->caller( __METHOD__ )
64            ->execute();
65    }
66
67    /**
68     * @param string $sourceLanguage
69     * @param array $titles
70     */
71    public function removeTitles( $sourceLanguage, array $titles ) {
72        if ( $titles === [] ) {
73            return;
74        }
75
76        /** @var LoadBalancer $lb */
77        $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' );
78        $dbw = $lb->getConnection( DB_PRIMARY );
79        $dbw->newDeleteQueryBuilder()
80            ->deleteFrom( 'cx_suggestions' )
81            ->where( [
82                'cxs_title' => $titles,
83                'cxs_source_language' => $sourceLanguage,
84            ] )
85            ->caller( __METHOD__ )
86            ->execute();
87    }
88
89    /**
90     * @param array $conds
91     * @return SuggestionList|null
92     */
93    protected function getListByConds( array $conds ) {
94        /** @var LoadBalancer $lb */
95        $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' );
96        $dbr = $lb->getConnection( DB_REPLICA );
97        $row = $dbr->newSelectQueryBuilder()
98            ->select( '*' )
99            ->from( 'cx_lists' )
100            ->where( $conds )
101            ->caller( __METHOD__ )
102            ->fetchRow();
103
104        if ( $row ) {
105            return SuggestionList::newFromRow( $row );
106        }
107
108        return null;
109    }
110
111    /**
112     * @param string $name
113     * @param int $owner
114     * @return SuggestionList|null
115     */
116    public function getListByName( $name, $owner = 0 ) {
117        $conds = [
118            'cxl_name' => $name,
119            'cxl_owner' => $owner,
120        ];
121
122        return $this->getListByConds( $conds );
123    }
124
125    /**
126     * @param int $id
127     * @return SuggestionList|null
128     */
129    public function getListById( $id ) {
130        $conds = [
131            'cxl_id' => $id,
132            'cxl_owner' => 0,
133        ];
134
135        return $this->getListByConds( $conds );
136    }
137
138    /**
139     * Get the titles discarded by the user between a language pair
140     *
141     * @param int $owner Owner's global user id.
142     * @param string $from Source language code.
143     * @param string $to Target language code.
144     * @return Title[]
145     */
146    public function getDiscardedSuggestions( $owner, $from, $to ) {
147        $titles = [];
148        $listName = 'cx-suggestionlist-discarded';
149
150        $suggestions = $this->getSuggestionsByListName( $owner, $listName, $from, $to );
151
152        foreach ( $suggestions as $suggestion ) {
153            $titles[] = $suggestion->getTitle();
154        }
155
156        return $titles;
157    }
158
159    /**
160     * Get suggestions markes as favorite by the translator.
161     *
162     * @param int $owner Owner's global user id.
163     * @return array Lists and suggestions
164     */
165    public function getFavoriteSuggestions( $owner ) {
166        $lists = [];
167        $listName = 'cx-suggestionlist-favorite';
168        $favoriteList = $this->getListByName( $listName, $owner );
169        $suggestions = $this->getSuggestionsByListName( $owner, $listName );
170
171        if ( $favoriteList ) {
172            $lists[] = $favoriteList;
173        }
174
175        return [
176            'lists' => $lists,
177            'suggestions' => $suggestions,
178        ];
179    }
180
181    /**
182     * Get the suggestions by list name for the given owner.
183     *
184     * @param int $owner Owner's global user id.
185     * @param string $listName
186     * @param string|null $from Source language code.
187     * @param string|null $to Target language code.
188     * @return Suggestion[] Suggestions
189     */
190    private function getSuggestionsByListName( $owner, $listName, $from = null, $to = null ) {
191        /** @var LoadBalancer $lb */
192        $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' );
193        $dbr = $lb->getConnection( DB_REPLICA );
194        $suggestions = [];
195        $conds = [
196            'cxl_name' => $listName,
197            'cxl_owner' => $owner,
198        ];
199
200        if ( $from !== null ) {
201            $conds[ 'cxs_source_language' ] = $from;
202        }
203        if ( $to !== null ) {
204            $conds[ 'cxs_target_language' ] = $to;
205        }
206
207        $res = $dbr->newSelectQueryBuilder()
208            ->select( [ 'cxs_list_id', 'cxs_title', 'cxs_source_language', 'cxs_target_language' ] )
209            ->from( 'cx_suggestions' )
210            ->join( 'cx_lists', null, 'cxs_list_id = cxl_id' )
211            ->where( $conds )
212            ->caller( __METHOD__ )
213            ->fetchResultSet();
214
215        foreach ( $res as $row ) {
216            $suggestions[] = Suggestion::newFromRow( $row );
217        }
218
219        return $suggestions;
220    }
221
222    /**
223     * Add suggestions to database.
224     *
225     * @param Suggestion[] $suggestions
226     */
227    public function addSuggestions( array $suggestions ) {
228        /** @var LoadBalancer $lb */
229        $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' );
230        $dbw = $lb->getConnection( DB_PRIMARY );
231
232        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
233
234        $batchSize = 100;
235        while ( count( $suggestions ) > 0 ) {
236            $batch = array_splice( $suggestions, 0, $batchSize );
237
238            $values = [];
239            foreach ( $batch as $suggestion ) {
240                $values[] = [
241                    'cxs_list_id' => $suggestion->getListId(),
242                    'cxs_title' => $suggestion->getTitle()->getPrefixedText(),
243                    'cxs_source_language' => $suggestion->getSourceLanguage(),
244                    'cxs_target_language' => $suggestion->getTargetLanguage(),
245                ];
246            }
247
248            $dbw->newInsertQueryBuilder()
249                ->insertInto( 'cx_suggestions' )
250                ->ignore()
251                ->rows( $values )
252                ->caller( __METHOD__ )
253                ->execute();
254
255            // TODO: This should really wait for replication on the
256            // Database returned by Database::getConnection( DB_PRIMARY );
257            $lbFactory->waitForReplication();
258        }
259    }
260
261    /**
262     * Remove each suggestions from the list it belongs to.
263     *
264     * @param Suggestion[] $suggestions
265     */
266    public function removeSuggestions( array $suggestions ) {
267        /** @var LoadBalancer $lb */
268        $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' );
269        $dbw = $lb->getConnection( DB_PRIMARY );
270
271        foreach ( $suggestions as $suggestion ) {
272            $values = [
273                'cxs_list_id' => $suggestion->getListId(),
274                'cxs_title' => $suggestion->getTitle()->getPrefixedText(),
275                'cxs_source_language' => $suggestion->getSourceLanguage(),
276                'cxs_target_language' => $suggestion->getTargetLanguage(),
277            ];
278            $dbw->newDeleteQueryBuilder()
279                ->deleteFrom( 'cx_suggestions' )
280                ->where( $values )
281                ->caller( __METHOD__ )
282                ->execute();
283        }
284    }
285
286    /**
287     * Check if suggestion exist in a list
288     *
289     * @param Suggestion $suggestion
290     * @return bool
291     */
292    public function doesSuggestionExist( Suggestion $suggestion ) {
293        /** @var LoadBalancer $lb */
294        $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' );
295        $dbr = $lb->getConnection( DB_REPLICA );
296
297        $row = $dbr->newSelectQueryBuilder()
298            ->select( '1' )
299            ->from( 'cx_suggestions' )
300            ->where( [
301                'cxs_list_id' => $suggestion->getListId(),
302                'cxs_title' => $suggestion->getTitle()->getPrefixedText(),
303                'cxs_source_language' => $suggestion->getSourceLanguage(),
304                'cxs_target_language' => $suggestion->getTargetLanguage(),
305            ] )
306            ->caller( __METHOD__ )
307            ->fetchRow();
308
309        // If there is no result, `fetchRow` returns `false`
310        return $row !== false;
311    }
312
313    /**
314     * Get public (non-personalized) suggestions.
315     *
316     * @param string $from Source language code.
317     * @param string $to Target language code.
318     * @param int $limit How many suggestions to fetch.
319     * @param int $offset Offset from the beginning to fetch.
320     * @param int $seed Seed to use with randomizing of results.
321     * @return array Lists and suggestions
322     */
323    public function getPublicSuggestions( $from, $to, $limit, $offset, $seed ) {
324        return $this->getSuggestionsByType(
325            [
326                SuggestionList::TYPE_CATEGORY,
327                SuggestionList::TYPE_FEATURED
328            ],
329            $from,
330            $to,
331            $limit,
332            $offset,
333            $seed
334        );
335    }
336
337    /**
338     * Get public suggestions by list type
339     *
340     * @param int|int[] $type List type.
341     * @param string $from Source language code.
342     * @param string $to Target language code.
343     * @param int $limit How many suggestions to fetch.
344     * @param int|null $offset Offset from the beginning to fetch.
345     * @param int|null $seed Seed to use with randomizing of results.
346     * @return array Lists and suggestions
347     */
348    public function getSuggestionsByType( $type, $from, $to, $limit, $offset = null, $seed = null ) {
349        /** @var LoadBalancer $lb */
350        $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' );
351        $dbr = $lb->getConnection( DB_REPLICA );
352
353        $lists = [];
354        $suggestions = [];
355
356        $res = $dbr->newSelectQueryBuilder()
357            ->select( '*' )
358            ->from( 'cx_lists' )
359            ->where( [
360                'cxl_type' => $type,
361                'cxl_public' => true,
362            ] )
363            ->caller( __METHOD__ )
364            ->orderBy( 'cxl_type', SelectQueryBuilder::SORT_DESC )
365            ->fetchResultSet();
366
367        foreach ( $res as $row ) {
368            $list = SuggestionList::newFromRow( $row );
369            $suggestionsInList = $this->getSuggestionsInList(
370                $list->getId(), $from, $to, $limit, $offset, $seed
371            );
372            if ( !count( $suggestionsInList ) ) {
373                continue;
374            }
375            $lists[$list->getId()] = $list;
376            $suggestions = array_merge(
377                $suggestions,
378                $suggestionsInList
379            );
380        }
381
382        return [
383            'lists' => $lists,
384            'suggestions' => $suggestions,
385        ];
386    }
387
388    /**
389     * Get the suggestions by list id
390     *
391     * @param int $listId
392     * @param string $from Source language code.
393     * @param string $to Target language code.
394     * @param int $limit How many suggestions to fetch.
395     * @param int $offset Offset from the beginning to fetch.
396     * @param int $seed Seed to use with randomizing of results.
397     * @return Suggestion[] Suggestions
398     */
399    public function getSuggestionsInList( $listId, $from, $to, $limit, $offset, $seed ) {
400        $suggestions = [];
401        /** @var LoadBalancer $lb */
402        $lb = MediaWikiServices::getInstance()->getService( 'ContentTranslation.LoadBalancer' );
403        $dbr = $lb->getConnection( DB_REPLICA );
404
405        $seed = (int)$seed;
406
407        $queryBuilder = $dbr->newSelectQueryBuilder()
408            ->select( '*' )
409            ->from( 'cx_suggestions' )
410            ->where( [
411                'cxs_source_language' => $from,
412                'cxs_target_language' => $to,
413                'cxs_list_id' => $listId
414            ] )
415            ->limit( $limit )
416            ->orderBy( "RAND( $seed )" )
417            ->caller( __METHOD__ );
418
419        if ( $offset ) {
420            $queryBuilder->offset( $offset );
421        }
422
423        $res = $queryBuilder->fetchResultSet();
424
425        foreach ( $res as $row ) {
426            $suggestions[] = Suggestion::newFromRow( $row );
427        }
428
429        return $suggestions;
430    }
431}