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