Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
21.94% |
68 / 310 |
|
15.79% |
3 / 19 |
CRAP | |
0.00% |
0 / 1 |
UncachedMenteeOverviewDataProvider | |
21.94% |
68 / 310 |
|
15.79% |
3 / 19 |
1144.08 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getReadConnection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
resetService | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getProfilingInfo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
storeProfilingData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getIds | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getLockedMenteesIds | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
getFilteredMenteesForMentor | |
0.00% |
0 / 90 |
|
0.00% |
0 / 1 |
56 | |||
getLastEditTimestampForUsersInternal | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
6 | |||
getLastEditTimestampForUsers | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
filterMenteesByLastEdit | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
getFormattedDataForMentor | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
getUsernames | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
getRevertedEditsForUsers | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getQuestionsAskedForUsers | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getTaggedEditsForUsers | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
30 | |||
getRegistrationTimestampForUsers | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
getBlocksForUsers | |
96.97% |
32 / 33 |
|
0.00% |
0 / 1 |
4 | |||
getEditCountsForUsers | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\MentorDashboard\MenteeOverview; |
4 | |
5 | use ChangeTags; |
6 | use ExtensionRegistry; |
7 | use GrowthExperiments\HomepageHooks; |
8 | use GrowthExperiments\HomepageModules\Mentorship; |
9 | use GrowthExperiments\Mentorship\MentorPageMentorManager; |
10 | use GrowthExperiments\Mentorship\Store\MentorStore; |
11 | use MediaWiki\Config\ServiceOptions; |
12 | use MediaWiki\Extension\CentralAuth\CentralAuthServices; |
13 | use MediaWiki\MainConfigNames; |
14 | use MediaWiki\Storage\NameTableAccessException; |
15 | use MediaWiki\Storage\NameTableStore; |
16 | use MediaWiki\User\ActorMigration; |
17 | use MediaWiki\User\TempUser\TempUserConfig; |
18 | use MediaWiki\User\UserIdentity; |
19 | use MediaWiki\User\UserIdentityLookup; |
20 | use Psr\Log\LoggerAwareTrait; |
21 | use Psr\Log\NullLogger; |
22 | use Wikimedia\Rdbms\IConnectionProvider; |
23 | use Wikimedia\Rdbms\IExpression; |
24 | use Wikimedia\Rdbms\IReadableDatabase; |
25 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
26 | |
27 | /** |
28 | * Provides data about interesting mentees mentored by a particular mentor |
29 | * |
30 | * WARNING: This class implements no caching, and it may be only used from CLI |
31 | * scripts or jobs. |
32 | */ |
33 | class UncachedMenteeOverviewDataProvider implements MenteeOverviewDataProvider { |
34 | use LoggerAwareTrait; |
35 | |
36 | public const CONSTRUCTOR_OPTIONS = [ |
37 | MainConfigNames::BlockTargetMigrationStage, |
38 | ]; |
39 | |
40 | /** @var int Number of seconds in a day */ |
41 | private const SECONDS_DAY = 86400; |
42 | |
43 | private ServiceOptions $options; |
44 | |
45 | private MentorStore $mentorStore; |
46 | |
47 | private NameTableStore $changeTagDefStore; |
48 | |
49 | private ActorMigration $actorMigration; |
50 | |
51 | private UserIdentityLookup $userIdentityLookup; |
52 | private TempUserConfig $tempUserConfig; |
53 | |
54 | private IConnectionProvider $mainConnProvider; |
55 | |
56 | /** @var array Cache used by getLastEditTimestampForUsers */ |
57 | private $lastTimestampCache = []; |
58 | |
59 | /** |
60 | * @var array Profiling information |
61 | * |
62 | * Stored by storeProfilingData, can be printed from |
63 | * updateMenteeData.php maintenance script. |
64 | */ |
65 | private $profilingInfo = []; |
66 | |
67 | /** |
68 | * @param ServiceOptions $options |
69 | * @param MentorStore $mentorStore |
70 | * @param NameTableStore $changeTagDefStore |
71 | * @param ActorMigration $actorMigration |
72 | * @param UserIdentityLookup $userIdentityLookup |
73 | * @param TempUserConfig $tempUserConfig |
74 | * @param IConnectionProvider $mainConnProvider |
75 | */ |
76 | public function __construct( |
77 | ServiceOptions $options, |
78 | MentorStore $mentorStore, |
79 | NameTableStore $changeTagDefStore, |
80 | ActorMigration $actorMigration, |
81 | UserIdentityLookup $userIdentityLookup, |
82 | TempUserConfig $tempUserConfig, |
83 | IConnectionProvider $mainConnProvider |
84 | ) { |
85 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
86 | $this->setLogger( new NullLogger() ); |
87 | |
88 | $this->options = $options; |
89 | $this->mentorStore = $mentorStore; |
90 | $this->changeTagDefStore = $changeTagDefStore; |
91 | $this->actorMigration = $actorMigration; |
92 | $this->userIdentityLookup = $userIdentityLookup; |
93 | $this->tempUserConfig = $tempUserConfig; |
94 | $this->mainConnProvider = $mainConnProvider; |
95 | } |
96 | |
97 | private function getReadConnection(): IReadableDatabase { |
98 | return $this->mainConnProvider->getReplicaDatabase( false, 'vslow' ); |
99 | } |
100 | |
101 | /** |
102 | * Do stuff that needs to happen before calculating the data |
103 | */ |
104 | private function resetService(): void { |
105 | $this->lastTimestampCache = []; |
106 | $this->profilingInfo = []; |
107 | } |
108 | |
109 | /** |
110 | * Get profiling information |
111 | * |
112 | * @internal Only use from updateMenteeData.php |
113 | * @return array |
114 | */ |
115 | public function getProfilingInfo(): array { |
116 | return $this->profilingInfo; |
117 | } |
118 | |
119 | /** |
120 | * @param string $section |
121 | * @param float $seconds |
122 | */ |
123 | private function storeProfilingData( string $section, float $seconds ): void { |
124 | $this->profilingInfo[$section] = $seconds; |
125 | } |
126 | |
127 | /** |
128 | * @param UserIdentity[] $users |
129 | * @return int[] |
130 | */ |
131 | private function getIds( array $users ): array { |
132 | return array_map( static function ( $user ) { |
133 | return $user->getId(); |
134 | }, $users ); |
135 | } |
136 | |
137 | /** |
138 | * Return local user IDs of all globally locked mentees |
139 | * |
140 | * If CentralAuth is not installed, this returns an empty array (as |
141 | * locking is not possible in that case). |
142 | * |
143 | * If CentralAuth is available, CentralAuthServices::getGlobalUserSelectQueryBuilderFactory |
144 | * is used to get locked mentees only. The select query builder does not impose any explicit |
145 | * limit on number of users that can be processed at once. UncachedMenteeOverviewDataProvider |
146 | * runs certain queries similar to the CentralAuth extension, which run fine with the current |
147 | * number of mentees (as of March 2022, the upper bound is 42,716). |
148 | * |
149 | * @param UserIdentity[] $mentees |
150 | * @return int[] |
151 | */ |
152 | private function getLockedMenteesIds( array $mentees ): array { |
153 | if ( |
154 | !ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) || |
155 | $mentees === [] |
156 | ) { |
157 | return []; |
158 | } |
159 | |
160 | if ( |
161 | !method_exists( CentralAuthServices::class, 'getGlobalUserSelectQueryBuilderFactory' ) |
162 | ) { |
163 | $this->logger->error( |
164 | 'Old version of CentralAuth found, CentralAuthServices::getGlobalUserSelectQueryBuilderFactory' . |
165 | ' was not found' |
166 | ); |
167 | return []; |
168 | } |
169 | |
170 | $userIdentities = CentralAuthServices::getGlobalUserSelectQueryBuilderFactory() |
171 | ->newGlobalUserSelectQueryBuilder() |
172 | ->whereUserNames( array_map( static function ( UserIdentity $user ) { |
173 | return $user->getName(); |
174 | }, $mentees ) ) |
175 | ->whereLocked( true ) |
176 | ->fetchLocalUserIdentitites(); |
177 | |
178 | return array_map( static function ( UserIdentity $user ) { |
179 | return $user->getId(); |
180 | }, iterator_to_array( $userIdentities ) ); |
181 | } |
182 | |
183 | /** |
184 | * Filter mentees according to business rules |
185 | * |
186 | * Only mentees that meet all of the following conditions |
187 | * should be considered: |
188 | * * user is not a bot |
189 | * * user is not a temporary account (safety check should temp users end up as mentees) |
190 | * * user is not indefinitely blocked |
191 | * * user is not globally locked (via CentralAuth's implementation) |
192 | * * user registered less than 2 weeks ago OR made at least one edit in the last 6 months |
193 | * |
194 | * @param UserIdentity $mentor |
195 | * @return int[] User IDs of the mentees |
196 | */ |
197 | private function getFilteredMenteesForMentor( UserIdentity $mentor ): array { |
198 | $startTime = microtime( true ); |
199 | |
200 | $mentees = $this->mentorStore->getMenteesByMentor( $mentor, MentorStore::ROLE_PRIMARY ); |
201 | $menteeIds = array_diff( |
202 | $this->getIds( $mentees ), |
203 | $this->getLockedMenteesIds( $mentees ) |
204 | ); |
205 | |
206 | if ( $menteeIds === [] ) { |
207 | return []; |
208 | } |
209 | |
210 | $dbr = $this->getReadConnection(); |
211 | |
212 | $queryBuilder = $dbr->newSelectQueryBuilder() |
213 | ->select( [ |
214 | 'user_id', |
215 | 'has_edits' => 'user_editcount > 0' |
216 | ] ) |
217 | ->from( 'user' ) |
218 | ->where( [ |
219 | // filter to mentees only |
220 | 'user_id' => $menteeIds, |
221 | |
222 | // ensure mentees have homepage enabled |
223 | 'user_id IN (' . $dbr->newSelectQueryBuilder() |
224 | ->select( 'up_user' ) |
225 | ->from( 'user_properties' ) |
226 | ->where( [ |
227 | 'up_property' => HomepageHooks::HOMEPAGE_PREF_ENABLE, |
228 | 'up_value' => '1', |
229 | ] ) |
230 | ->getSQL() . |
231 | ')', |
232 | |
233 | // ensure mentees do not have mentorship disabled |
234 | 'user_id NOT IN (' . $dbr->newSelectQueryBuilder() |
235 | ->select( 'up_user' ) |
236 | ->from( 'user_properties' ) |
237 | ->where( [ |
238 | 'up_property' => MentorPageMentorManager::MENTORSHIP_ENABLED_PREF, |
239 | // sanity check, should never match (1 is the default value) |
240 | $dbr->expr( 'up_value', '!=', '1' ), |
241 | ] ) |
242 | ->getSQL() . |
243 | ')', |
244 | |
245 | // user is not a bot, |
246 | 'user_id NOT IN (' . $dbr->newSelectQueryBuilder() |
247 | ->select( 'ug_user' ) |
248 | ->from( 'user_groups' ) |
249 | ->where( [ 'ug_group' => 'bot' ] ) |
250 | ->getSQL() . |
251 | ')', |
252 | |
253 | // only users who either made an edit or registered less than 2 weeks ago |
254 | $dbr->expr( 'user_editcount', '>', 0 ) |
255 | ->or( 'user_registration', '>', $dbr->timestamp( |
256 | (int)wfTimestamp( TS_UNIX ) - 2 * 7 * self::SECONDS_DAY |
257 | ) ), |
258 | ] ) |
259 | ->caller( __METHOD__ ); |
260 | |
261 | // Exclude users which are indefinitely blocked |
262 | if ( $this->options->get( MainConfigNames::BlockTargetMigrationStage ) & SCHEMA_COMPAT_READ_OLD ) { |
263 | $queryBuilder->andWhere( |
264 | 'user_id NOT IN (' . $dbr->newSelectQueryBuilder() |
265 | ->select( 'ipb_user' ) |
266 | ->from( 'ipblocks' ) |
267 | ->where( [ |
268 | 'ipb_expiry' => $dbr->getInfinity(), |
269 | // not an IP block |
270 | $dbr->expr( 'ipb_user', '!=', 0 ), |
271 | ] ) |
272 | ->getSQL() . |
273 | ')' |
274 | ); |
275 | } else { |
276 | $queryBuilder->andWhere( |
277 | 'user_id NOT IN (' . $dbr->newSelectQueryBuilder() |
278 | ->select( 'bt_user' ) |
279 | ->from( 'block' ) |
280 | ->join( 'block_target', null, [ |
281 | 'bt_id=bl_target' |
282 | ] ) |
283 | ->where( [ |
284 | 'bl_expiry' => $dbr->getInfinity(), |
285 | // not an IP block |
286 | $dbr->expr( 'bt_user', '!=', null ) |
287 | ] ) |
288 | ->getSQL() . |
289 | ')' |
290 | ); |
291 | } |
292 | |
293 | // exclude temporary accounts, if enabled (T341389) |
294 | if ( $this->tempUserConfig->isEnabled() ) { |
295 | foreach ( $this->tempUserConfig->getMatchPatterns() as $pattern ) { |
296 | $queryBuilder->andWhere( |
297 | $dbr->expr( 'user_name', IExpression::NOT_LIKE, $pattern->toLikeValue( $dbr ) ) |
298 | ); |
299 | } |
300 | } |
301 | |
302 | $res = $queryBuilder->fetchResultSet(); |
303 | |
304 | $editingUsers = []; |
305 | $notEditingUsers = []; |
306 | foreach ( $res as $row ) { |
307 | if ( $row->has_edits ) { |
308 | $editingUsers[] = (int)$row->user_id; |
309 | } else { |
310 | $notEditingUsers[] = (int)$row->user_id; |
311 | } |
312 | } |
313 | |
314 | $this->storeProfilingData( 'filtermentees', microtime( true ) - $startTime ); |
315 | |
316 | return array_merge( |
317 | $notEditingUsers, |
318 | $this->filterMenteesByLastEdit( $editingUsers ) |
319 | ); |
320 | } |
321 | |
322 | /** |
323 | * @param int[] $userIds |
324 | * @return array |
325 | */ |
326 | private function getLastEditTimestampForUsersInternal( array $userIds ): array { |
327 | $startTime = microtime( true ); |
328 | |
329 | $rows = $this->getReadConnection()->newSelectQueryBuilder() |
330 | ->select( [ |
331 | 'actor_user', |
332 | 'last_edit' => 'MAX(rev_timestamp)' |
333 | ] ) |
334 | ->from( 'revision' ) |
335 | ->join( 'actor', null, 'rev_actor = actor_id' ) |
336 | ->where( [ |
337 | 'actor_user' => $userIds, |
338 | ] ) |
339 | ->caller( __METHOD__ ) |
340 | ->groupBy( 'actor_user' ) |
341 | ->fetchResultSet(); |
342 | $res = []; |
343 | foreach ( $rows as $row ) { |
344 | $res[$row->actor_user] = $row->last_edit; |
345 | } |
346 | |
347 | $this->storeProfilingData( |
348 | 'edittimestampinternal', |
349 | microtime( true ) - $startTime |
350 | ); |
351 | return $res; |
352 | } |
353 | |
354 | /** |
355 | * @param int[] $userIds |
356 | * @return array |
357 | */ |
358 | private function getLastEditTimestampForUsers( array $userIds ): array { |
359 | if ( $userIds === [] ) { |
360 | return []; |
361 | } |
362 | |
363 | $data = array_intersect_key( $this->lastTimestampCache, array_fill_keys( $userIds, true ) ); |
364 | $notInCache = array_diff( $userIds, array_keys( $this->lastTimestampCache ) ); |
365 | if ( $notInCache ) { |
366 | $new = $this->getLastEditTimestampForUsersInternal( $notInCache ); |
367 | $data += $new; |
368 | $this->lastTimestampCache += $new; |
369 | } |
370 | return $data; |
371 | } |
372 | |
373 | /** |
374 | * Filter provided user IDs to IDs of users who edited up to 6 months ago |
375 | * |
376 | * @param array $allUserIds |
377 | * @return int[] |
378 | */ |
379 | private function filterMenteesByLastEdit( array $allUserIds ): array { |
380 | if ( $allUserIds === [] ) { |
381 | return []; |
382 | } |
383 | |
384 | $allLastEdits = $this->getLastEditTimestampForUsers( $allUserIds ); |
385 | $userIds = []; |
386 | foreach ( $allLastEdits as $userId => $lastEdit ) { |
387 | $secondsSinceLastEdit = (int)wfTimestamp( TS_UNIX ) - |
388 | (int)ConvertibleTimestamp::convert( |
389 | TS_UNIX, |
390 | $lastEdit |
391 | ); |
392 | if ( $secondsSinceLastEdit <= self::SECONDS_DAY * 6 * 30 ) { |
393 | $userIds[] = $userId; |
394 | } |
395 | } |
396 | return $userIds; |
397 | } |
398 | |
399 | /** |
400 | * Calculates data for a given mentor's mentees |
401 | * |
402 | * @param UserIdentity $mentor |
403 | * @return array |
404 | */ |
405 | public function getFormattedDataForMentor( UserIdentity $mentor ): array { |
406 | $this->resetService(); |
407 | |
408 | $userIds = $this->getFilteredMenteesForMentor( $mentor ); |
409 | if ( $userIds === [] ) { |
410 | return []; |
411 | } |
412 | |
413 | $mainData = [ |
414 | 'username' => $this->getUsernames( $userIds ), |
415 | 'reverted' => $this->getRevertedEditsForUsers( $userIds ), |
416 | 'questions' => $this->getQuestionsAskedForUsers( $userIds ), |
417 | 'editcount' => $this->getEditCountsForUsers( $userIds ), |
418 | 'registration' => $this->getRegistrationTimestampForUsers( $userIds ), |
419 | 'last_edit' => $this->getLastEditTimestampForUsers( $userIds ), |
420 | 'blocks' => $this->getBlocksForUsers( $userIds ), |
421 | ]; |
422 | |
423 | $res = []; |
424 | foreach ( $mainData as $key => $data ) { |
425 | foreach ( $data as $userId => $value ) { |
426 | $res[$userId][$key] = $value; |
427 | } |
428 | } |
429 | foreach ( $res as $userId => $userData ) { |
430 | $res[$userId]['last_active'] = $userData['last_edit'] ?? $userData['registration']; |
431 | } |
432 | return $res; |
433 | } |
434 | |
435 | /** |
436 | * @param int[] $userIds |
437 | * @return array |
438 | */ |
439 | private function getUsernames( array $userIds ): array { |
440 | $startTime = microtime( true ); |
441 | |
442 | $rows = $this->getReadConnection()->newSelectQueryBuilder() |
443 | ->select( [ 'user_id', 'user_name' ] ) |
444 | ->from( 'user' ) |
445 | ->where( [ 'user_id' => $userIds ] ) |
446 | ->caller( __METHOD__ ) |
447 | ->fetchResultSet(); |
448 | $res = []; |
449 | foreach ( $rows as $row ) { |
450 | $res[$row->user_id] = $row->user_name; |
451 | } |
452 | |
453 | $this->storeProfilingData( 'usernames', microtime( true ) - $startTime ); |
454 | |
455 | return $res; |
456 | } |
457 | |
458 | /** |
459 | * @param int[] $userIds |
460 | * @return array |
461 | */ |
462 | private function getRevertedEditsForUsers( array $userIds ): array { |
463 | $startTime = microtime( true ); |
464 | $res = $this->getTaggedEditsForUsers( |
465 | [ ChangeTags::TAG_REVERTED ], |
466 | $userIds |
467 | ); |
468 | $this->storeProfilingData( 'reverted', microtime( true ) - $startTime ); |
469 | return $res; |
470 | } |
471 | |
472 | /** |
473 | * @param int[] $userIds |
474 | * @return array |
475 | */ |
476 | private function getQuestionsAskedForUsers( array $userIds ): array { |
477 | $startTime = microtime( true ); |
478 | $res = $this->getTaggedEditsForUsers( |
479 | [ |
480 | Mentorship::MENTORSHIP_HELPPANEL_QUESTION_TAG, |
481 | Mentorship::MENTORSHIP_MODULE_QUESTION_TAG |
482 | ], |
483 | $userIds |
484 | ); |
485 | $this->storeProfilingData( 'questions', microtime( true ) - $startTime ); |
486 | return $res; |
487 | } |
488 | |
489 | /** |
490 | * @param string[] $tags |
491 | * @param int[] $userIds |
492 | * @return int[] |
493 | */ |
494 | private function getTaggedEditsForUsers( array $tags, array $userIds ) { |
495 | $tagIds = []; |
496 | foreach ( $tags as $tag ) { |
497 | try { |
498 | $tagIds[] = $this->changeTagDefStore->getId( $tag ); |
499 | } catch ( NameTableAccessException $e ) { |
500 | // Skip non-existing tags gracefully |
501 | } |
502 | } |
503 | if ( $tagIds === [] ) { |
504 | return array_fill_keys( $userIds, 0 ); |
505 | } |
506 | |
507 | $dbr = $this->getReadConnection(); |
508 | $queryInfo = $this->actorMigration->getJoin( 'rev_user' ); |
509 | $taggedEditsSubquery = $dbr->newSelectQueryBuilder() |
510 | ->select( [ |
511 | 'rev_user' => $queryInfo['fields']['rev_user'], |
512 | 'ct_rev_id' |
513 | ] ) |
514 | ->from( 'change_tag' ) |
515 | ->join( 'revision', null, 'rev_id=ct_rev_id' ) |
516 | ->tables( $queryInfo['tables'] ) |
517 | ->where( [ |
518 | 'actor_user' => $userIds, |
519 | 'ct_tag_id' => $tagIds |
520 | ] ) |
521 | ->joinConds( $queryInfo['joins'] ) |
522 | ->caller( __METHOD__ ); |
523 | $rows = $dbr->newSelectQueryBuilder() |
524 | ->select( [ 'user_id', 'tagged' => 'COUNT(ct_rev_id)' ] ) |
525 | ->from( 'user' ) |
526 | ->leftJoin( $taggedEditsSubquery, 'tagged_edits', 'rev_user=user_id' ) |
527 | ->where( [ 'user_id' => $userIds ] ) |
528 | ->caller( __METHOD__ ) |
529 | ->groupBy( 'user_id' ) |
530 | ->fetchResultSet(); |
531 | |
532 | $res = []; |
533 | foreach ( $rows as $row ) { |
534 | $res[$row->user_id] = (int)$row->tagged; |
535 | } |
536 | return $res; |
537 | } |
538 | |
539 | /** |
540 | * @param int[] $userIds |
541 | * @return array |
542 | */ |
543 | private function getRegistrationTimestampForUsers( array $userIds ): array { |
544 | $startTime = microtime( true ); |
545 | $rows = $this->getReadConnection()->newSelectQueryBuilder() |
546 | ->select( [ 'user_id', 'user_registration' ] ) |
547 | ->from( 'user' ) |
548 | ->where( [ 'user_id' => $userIds ] ) |
549 | ->caller( __METHOD__ ) |
550 | ->fetchResultSet(); |
551 | $res = []; |
552 | foreach ( $rows as $row ) { |
553 | $res[$row->user_id] = $row->user_registration; |
554 | } |
555 | $this->storeProfilingData( 'registration', microtime( true ) - $startTime ); |
556 | return $res; |
557 | } |
558 | |
559 | /** |
560 | * Get number of blocks placed against the mentees |
561 | * |
562 | * @param int[] $userIds |
563 | * @return int[] |
564 | */ |
565 | private function getBlocksForUsers( array $userIds ): array { |
566 | if ( $userIds === [] ) { |
567 | return []; |
568 | } |
569 | |
570 | $startTime = microtime( true ); |
571 | |
572 | // fetch usernames (assoc. array; username => user ID) |
573 | // NOTE: username has underscores, not spaces |
574 | $users = []; |
575 | $userIdentities = iterator_to_array( $this->userIdentityLookup->newSelectQueryBuilder() |
576 | ->whereUserIds( $userIds ) |
577 | ->fetchUserIdentities() ); |
578 | array_walk( |
579 | $userIdentities, |
580 | static function ( UserIdentity $value, $key ) use ( &$users ) { |
581 | $users[str_replace( ' ', '_', $value->getName() )] = $value->getId(); |
582 | } |
583 | ); |
584 | |
585 | $rows = $this->getReadConnection()->newSelectQueryBuilder() |
586 | ->select( [ 'log_title', 'blocks' => 'COUNT(log_id)' ] ) |
587 | ->from( 'logging' ) |
588 | ->where( [ |
589 | 'log_type' => 'block', |
590 | 'log_action' => 'block', |
591 | 'log_namespace' => NS_USER, |
592 | 'log_title' => array_keys( $users ) |
593 | ] ) |
594 | ->groupBy( 'log_title' ) |
595 | ->caller( __METHOD__ ) |
596 | ->fetchResultSet(); |
597 | |
598 | $res = []; |
599 | foreach ( $rows as $row ) { |
600 | $res[$users[$row->log_title]] = (int)$row->blocks; |
601 | } |
602 | |
603 | // fill missing IDs with zeros |
604 | $missingIds = array_diff( $userIds, array_keys( $res ) ); |
605 | foreach ( $missingIds as $id ) { |
606 | $res[$id] = 0; |
607 | } |
608 | |
609 | $this->storeProfilingData( 'blocks', microtime( true ) - $startTime ); |
610 | |
611 | return $res; |
612 | } |
613 | |
614 | /** |
615 | * @param int[] $userIds |
616 | * @return int[] |
617 | */ |
618 | private function getEditCountsForUsers( array $userIds ): array { |
619 | $startTime = microtime( true ); |
620 | |
621 | $rows = $this->getReadConnection()->newSelectQueryBuilder() |
622 | ->select( [ 'user_id', 'user_editcount' ] ) |
623 | ->from( 'user' ) |
624 | ->where( [ 'user_id' => $userIds ] ) |
625 | ->caller( __METHOD__ ) |
626 | ->fetchResultSet(); |
627 | $res = []; |
628 | foreach ( $rows as $row ) { |
629 | $res[$row->user_id] = (int)$row->user_editcount; |
630 | } |
631 | |
632 | $this->storeProfilingData( 'editcount', microtime( true ) - $startTime ); |
633 | |
634 | return $res; |
635 | } |
636 | } |