Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
63.91% |
170 / 266 |
|
15.79% |
3 / 19 |
CRAP | |
0.00% |
0 / 1 |
SpecialManageMentors | |
63.91% |
170 / 266 |
|
15.79% |
3 / 19 |
173.27 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isIncludable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renderInReadOnlyMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canManageMentors | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLastActiveTimestamp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
makeUserLink | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
formatWeight | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 | |||
formatStatus | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
getMentorAsHtmlRow | |
100.00% |
41 / 41 |
|
100.00% |
1 / 1 |
2 | |||
getMentorsTableBody | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
3.00 | |||
getMentorsTable | |
100.00% |
67 / 67 |
|
100.00% |
1 / 1 |
3 | |||
getFormByAction | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
4.00 | |||
parseSubpage | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
4.13 | |||
handleAction | |
47.37% |
9 / 19 |
|
0.00% |
0 / 1 |
8.64 | |||
makePreHTML | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
12 | |||
makeHeadlineElement | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
execute | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Specials; |
4 | |
5 | use GrowthExperiments\MentorDashboard\MentorTools\IMentorWeights; |
6 | use GrowthExperiments\MentorDashboard\MentorTools\MentorStatusManager; |
7 | use GrowthExperiments\Mentorship\Mentor; |
8 | use GrowthExperiments\Mentorship\MentorRemover; |
9 | use GrowthExperiments\Mentorship\Provider\IMentorWriter; |
10 | use GrowthExperiments\Mentorship\Provider\MentorProvider; |
11 | use GrowthExperiments\Specials\Forms\ManageMentorsAbstractForm; |
12 | use GrowthExperiments\Specials\Forms\ManageMentorsEditMentor; |
13 | use GrowthExperiments\Specials\Forms\ManageMentorsRemoveMentor; |
14 | use HTMLForm; |
15 | use LogicException; |
16 | use MediaWiki\Html\Html; |
17 | use MediaWiki\Linker\Linker; |
18 | use MediaWiki\SpecialPage\SpecialPage; |
19 | use MediaWiki\User\UserEditTracker; |
20 | use MediaWiki\User\UserIdentity; |
21 | use MediaWiki\User\UserIdentityLookup; |
22 | use MediaWiki\Utils\MWTimestamp; |
23 | use OOUI\ButtonWidget; |
24 | use PermissionsError; |
25 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
26 | |
27 | class SpecialManageMentors extends SpecialPage { |
28 | |
29 | private UserIdentityLookup $userIdentityLookup; |
30 | private UserEditTracker $userEditTracker; |
31 | private MentorProvider $mentorProvider; |
32 | private IMentorWriter $mentorWriter; |
33 | private MentorStatusManager $mentorStatusManager; |
34 | private MentorRemover $mentorRemover; |
35 | |
36 | /** |
37 | * @param UserIdentityLookup $userIdentityLookup |
38 | * @param UserEditTracker $userEditTracker |
39 | * @param MentorProvider $mentorProvider |
40 | * @param IMentorWriter $mentorWriter |
41 | * @param MentorStatusManager $mentorStatusManager |
42 | * @param MentorRemover $mentorRemover |
43 | */ |
44 | public function __construct( |
45 | UserIdentityLookup $userIdentityLookup, |
46 | UserEditTracker $userEditTracker, |
47 | MentorProvider $mentorProvider, |
48 | IMentorWriter $mentorWriter, |
49 | MentorStatusManager $mentorStatusManager, |
50 | MentorRemover $mentorRemover |
51 | ) { |
52 | parent::__construct( 'ManageMentors' ); |
53 | |
54 | $this->userIdentityLookup = $userIdentityLookup; |
55 | $this->userEditTracker = $userEditTracker; |
56 | $this->mentorProvider = $mentorProvider; |
57 | $this->mentorWriter = $mentorWriter; |
58 | $this->mentorStatusManager = $mentorStatusManager; |
59 | $this->mentorRemover = $mentorRemover; |
60 | } |
61 | |
62 | /** @inheritDoc */ |
63 | protected function getGroupName() { |
64 | return 'growth-tools'; |
65 | } |
66 | |
67 | /** |
68 | * @inheritDoc |
69 | */ |
70 | public function isIncludable() { |
71 | return true; |
72 | } |
73 | |
74 | /** |
75 | * @return bool |
76 | */ |
77 | private function renderInReadOnlyMode(): bool { |
78 | return $this->including() ?? false; |
79 | } |
80 | |
81 | /** |
82 | * Can manage mentors? |
83 | * @return bool |
84 | */ |
85 | private function canManageMentors(): bool { |
86 | return !$this->renderInReadOnlyMode() && |
87 | ManageMentorsAbstractForm::canManageMentors( $this->getAuthority() ); |
88 | } |
89 | |
90 | /** |
91 | * @inheritDoc |
92 | */ |
93 | public function getDescription() { |
94 | return $this->msg( 'growthexperiments-manage-mentors-title' ); |
95 | } |
96 | |
97 | /** |
98 | * @param UserIdentity $user |
99 | * @return MWTimestamp |
100 | */ |
101 | private function getLastActiveTimestamp( UserIdentity $user ): MWTimestamp { |
102 | return new MWTimestamp( $this->userEditTracker->getLatestEditTimestamp( $user ) ); |
103 | } |
104 | |
105 | private function makeUserLink( UserIdentity $user ) { |
106 | return Linker::userLink( |
107 | $user->getId(), |
108 | $user->getName() |
109 | ) . Linker::userToolLinks( $user->getId(), $user->getName() ); |
110 | } |
111 | |
112 | /** |
113 | * @param Mentor $mentor |
114 | * @return array{0:string,1:int} |
115 | */ |
116 | private function formatWeight( Mentor $mentor ): array { |
117 | switch ( $mentor->getWeight() ) { |
118 | case IMentorWeights::WEIGHT_NONE: |
119 | $msgKey = 'growthexperiments-mentor-dashboard-mentor-tools-mentor-weight-none'; |
120 | break; |
121 | case IMentorWeights::WEIGHT_LOW: |
122 | $msgKey = 'growthexperiments-mentor-dashboard-mentor-tools-mentor-weight-low'; |
123 | break; |
124 | case IMentorWeights::WEIGHT_NORMAL: |
125 | $msgKey = 'growthexperiments-mentor-dashboard-mentor-tools-mentor-weight-medium'; |
126 | break; |
127 | case IMentorWeights::WEIGHT_HIGH: |
128 | $msgKey = 'growthexperiments-mentor-dashboard-mentor-tools-mentor-weight-high'; |
129 | break; |
130 | default: |
131 | throw new LogicException( |
132 | 'Weight ' . $mentor->getWeight() . ' is not supported' |
133 | ); |
134 | } |
135 | return [ $this->msg( $msgKey )->text(), $mentor->getWeight() ]; |
136 | } |
137 | |
138 | /** |
139 | * @param Mentor $mentor |
140 | * @return array{0:string,1:int} |
141 | */ |
142 | private function formatStatus( Mentor $mentor ): array { |
143 | $reason = $this->mentorStatusManager->getAwayReason( $mentor->getUserIdentity() ); |
144 | switch ( $reason ) { |
145 | case MentorStatusManager::AWAY_BECAUSE_BLOCK: |
146 | case MentorStatusManager::AWAY_BECAUSE_LOCK: |
147 | return [ |
148 | // FIXME use better custom message |
149 | $this->msg( 'blockedtitle' )->text(), |
150 | // XXX: is this the maximum on the frontend? |
151 | PHP_INT_MAX |
152 | ]; |
153 | case MentorStatusManager::AWAY_BECAUSE_TIMESTAMP: |
154 | $ts = $this->mentorStatusManager->getMentorBackTimestamp( $mentor->getUserIdentity() ); |
155 | if ( $ts !== null ) { |
156 | return [ |
157 | $this->msg( 'growthexperiments-manage-mentors-status-away-until' ) |
158 | ->params( $this->getLanguage()->userDate( $ts, $this->getUser() ) ) |
159 | ->text(), |
160 | (int)ConvertibleTimestamp::convert( TS_UNIX, $ts ) |
161 | ]; |
162 | } |
163 | // if the reason is a timestamp, but we've got no timestamp, just pretend they are active |
164 | // hence no break here |
165 | case null: |
166 | return [ |
167 | $this->msg( 'growthexperiments-mentor-dashboard-mentor-tools-mentor-status-active' ) |
168 | ->text(), |
169 | -1 |
170 | ]; |
171 | default: |
172 | throw new LogicException( "Reason for absence \"$reason\" is not supported" ); |
173 | } |
174 | } |
175 | |
176 | /** |
177 | * @param Mentor $mentor |
178 | * @param int $i |
179 | * @return string |
180 | */ |
181 | private function getMentorAsHtmlRow( Mentor $mentor, int $i ): string { |
182 | [ $weightText, $weightRank ] = $this->formatWeight( $mentor ); |
183 | [ $statusText, $statusRank ] = $this->formatStatus( $mentor ); |
184 | $ts = $this->getLastActiveTimestamp( $mentor->getUserIdentity() ); |
185 | |
186 | $items = [ |
187 | Html::element( 'td', [], (string)$i ), |
188 | Html::rawElement( |
189 | 'td', |
190 | [ 'data-sort-value' => $mentor->getUserIdentity()->getName() ], |
191 | $this->makeUserLink( $mentor->getUserIdentity() ) |
192 | ), |
193 | Html::element( |
194 | 'td', |
195 | [ 'data-sort-value' => $ts->getTimestamp( TS_UNIX ) ], |
196 | $this->getContext()->getLanguage()->userTimeAndDate( $ts, $this->getUser() ) |
197 | ), |
198 | Html::element( 'td', [ 'data-sort-value' => $weightRank ], $weightText ), |
199 | Html::element( 'td', [ 'data-sort-value' => $statusRank ], $statusText ), |
200 | Html::element( 'td', [], $mentor->getIntroText() ), |
201 | ]; |
202 | if ( $this->canManageMentors() ) { |
203 | $items[] = Html::rawElement( 'td', [], new ButtonWidget( [ |
204 | 'label' => $this->msg( 'growthexperiments-manage-mentors-edit' )->text(), |
205 | 'href' => SpecialPage::getTitleFor( |
206 | 'ManageMentors', |
207 | 'edit-mentor/' . $mentor->getUserIdentity()->getId() |
208 | )->getLocalURL(), |
209 | 'flags' => [ 'primary', 'progressive' ], |
210 | ] ) ); |
211 | $items[] = Html::rawElement( 'td', [], new ButtonWidget( [ |
212 | 'label' => $this->msg( 'growthexperiments-manage-mentors-remove-mentor' )->text(), |
213 | 'href' => SpecialPage::getTitleFor( |
214 | 'ManageMentors', |
215 | 'remove-mentor/' . $mentor->getUserIdentity()->getId() |
216 | )->getLocalURL(), |
217 | 'flags' => [ 'primary', 'destructive' ] |
218 | ] ) ); |
219 | } |
220 | |
221 | return Html::rawElement( |
222 | 'tr', |
223 | [], |
224 | implode( "\n", $items ) |
225 | ); |
226 | } |
227 | |
228 | /** |
229 | * @param string[] $mentorNames |
230 | * @return string |
231 | */ |
232 | private function getMentorsTableBody( array $mentorNames ): string { |
233 | // sort mentors alphabetically |
234 | sort( $mentorNames ); |
235 | |
236 | $mentorsHtml = []; |
237 | $i = 1; |
238 | foreach ( $mentorNames as $mentorName ) { |
239 | $mentorUser = $this->userIdentityLookup->getUserIdentityByName( $mentorName ); |
240 | if ( !$mentorUser ) { |
241 | // TODO: Log an error? |
242 | continue; |
243 | } |
244 | |
245 | $mentorsHtml[] = $this->getMentorAsHtmlRow( |
246 | $this->mentorProvider->newMentorFromUserIdentity( $mentorUser ), |
247 | $i |
248 | ); |
249 | $i++; |
250 | } |
251 | |
252 | return implode( "\n", $mentorsHtml ); |
253 | } |
254 | |
255 | /** |
256 | * @param string[] $mentorNames |
257 | * @return string |
258 | */ |
259 | private function getMentorsTable( array $mentorNames ): string { |
260 | if ( $mentorNames === [] ) { |
261 | return Html::element( |
262 | 'p', |
263 | [], |
264 | $this->msg( 'growthexperiments-manage-mentors-none' )->text() |
265 | ); |
266 | } |
267 | |
268 | $headerItems = [ |
269 | Html::element( 'th', [], '#' ), |
270 | Html::element( |
271 | 'th', |
272 | [], |
273 | $this->msg( 'growthexperiments-manage-mentors-username' )->text() |
274 | ), |
275 | Html::element( |
276 | 'th', |
277 | // unix timestamp |
278 | [ 'data-sort-type' => 'number' ], |
279 | $this->msg( 'growthexperiments-manage-mentors-last-active' )->text() |
280 | ), |
281 | Html::element( |
282 | 'th', |
283 | [ 'data-sort-type' => 'number' ], |
284 | $this->msg( 'growthexperiments-manage-mentors-weight' )->text() |
285 | ), |
286 | Html::element( |
287 | 'th', |
288 | [ 'data-sort-type' => 'number' ], |
289 | $this->msg( 'growthexperiments-manage-mentors-status' )->text() |
290 | ), |
291 | Html::element( |
292 | 'th', |
293 | [ 'class' => 'unsortable' ], |
294 | $this->msg( 'growthexperiments-manage-mentors-intro-msg' )->text() |
295 | ), |
296 | ]; |
297 | |
298 | if ( $this->canManageMentors() ) { |
299 | $headerItems[] = Html::element( |
300 | 'th', |
301 | [ 'class' => 'unsortable' ], |
302 | $this->msg( 'growthexperiments-manage-mentors-edit' )->text() |
303 | ); |
304 | $headerItems[] = Html::element( |
305 | 'th', |
306 | [ 'class' => 'unsortable' ], |
307 | $this->msg( 'growthexperiments-manage-mentors-remove-mentor' )->text() |
308 | ); |
309 | } |
310 | |
311 | return Html::rawElement( |
312 | 'table', |
313 | [ |
314 | 'class' => 'wikitable sortable' |
315 | ], |
316 | implode( "\n", [ |
317 | Html::rawElement( |
318 | 'thead', |
319 | [], |
320 | Html::rawElement( |
321 | 'tr', |
322 | [], |
323 | implode( "\n", $headerItems ) |
324 | ) |
325 | ), |
326 | Html::rawElement( |
327 | 'tbody', |
328 | [], |
329 | $this->getMentorsTableBody( $mentorNames ) |
330 | ) |
331 | ] ) |
332 | ); |
333 | } |
334 | |
335 | /** |
336 | * @param string $action |
337 | * @param UserIdentity $mentorUser |
338 | * @return HTMLForm|null |
339 | */ |
340 | private function getFormByAction( string $action, UserIdentity $mentorUser ): ?HTMLForm { |
341 | switch ( $action ) { |
342 | case 'remove-mentor': |
343 | return new ManageMentorsRemoveMentor( |
344 | $this->mentorRemover, |
345 | $mentorUser, |
346 | $this->getContext() |
347 | ); |
348 | case 'edit-mentor': |
349 | return new ManageMentorsEditMentor( |
350 | $this->mentorProvider, |
351 | $this->mentorWriter, |
352 | $this->mentorStatusManager, |
353 | $mentorUser, |
354 | $this->getContext() |
355 | ); |
356 | default: |
357 | return null; |
358 | } |
359 | } |
360 | |
361 | private function parseSubpage( ?string $par ): ?array { |
362 | if ( !$par || strpos( $par, '/' ) === false ) { |
363 | return null; |
364 | } |
365 | |
366 | [ $action, $data ] = explode( '/', $par, 2 ); |
367 | $mentorUserId = (int)$data; |
368 | if ( !$mentorUserId ) { |
369 | return null; |
370 | } |
371 | |
372 | return [ |
373 | $action, |
374 | $this->userIdentityLookup->getUserIdentityByUserId( $mentorUserId ) |
375 | ]; |
376 | } |
377 | |
378 | private function handleAction( ?string $par ): bool { |
379 | [ $action, $mentorUser ] = $this->parseSubpage( $par ); |
380 | |
381 | if ( !$action ) { |
382 | return false; |
383 | } |
384 | |
385 | if ( !$this->canManageMentors() ) { |
386 | throw new PermissionsError( 'managementors' ); |
387 | } |
388 | |
389 | if ( !$mentorUser ) { |
390 | $this->getOutput()->addHTML( Html::element( |
391 | 'p', |
392 | [ 'class' => 'error' ], |
393 | $this->msg( |
394 | 'growthexperiments-manage-mentors-error-no-such-user' |
395 | )->text() |
396 | ) ); |
397 | return true; |
398 | } |
399 | |
400 | $form = $this->getFormByAction( $action, $mentorUser ); |
401 | if ( !$form ) { |
402 | return false; |
403 | } |
404 | |
405 | $form->show(); |
406 | return true; |
407 | } |
408 | |
409 | /** |
410 | * @return string |
411 | */ |
412 | private function makePreHTML(): string { |
413 | if ( $this->including() ) { |
414 | // included version should only include the table |
415 | return ''; |
416 | } |
417 | |
418 | $howToChangeMessageKey = $this->canManageMentors() |
419 | ? 'growthexperiments-manage-mentors-pretext-privileged' |
420 | : 'growthexperiments-manage-mentors-pretext-regular'; |
421 | |
422 | return Html::rawElement( |
423 | 'div', |
424 | [], |
425 | implode( "\n", [ |
426 | Html::rawElement( |
427 | 'p', |
428 | [], |
429 | implode( "\n", [ |
430 | $this->msg( 'growthexperiments-manage-mentors-pretext-purpose' )->parse(), |
431 | $this->msg( $howToChangeMessageKey )->parse(), |
432 | $this->msg( 'growthexperiments-manage-mentors-pretext-stored-at' ) |
433 | ->params( $this->getConfig()->get( 'GEStructuredMentorList' ) ) |
434 | ->parse(), |
435 | ] ) |
436 | ), |
437 | Html::rawElement( |
438 | 'p', |
439 | [], |
440 | $this->msg( 'growthexperiments-manage-mentors-pretext-to-enroll' )->parse() |
441 | ) |
442 | ] ) |
443 | ); |
444 | } |
445 | |
446 | /** |
447 | * @param string $text |
448 | * @return string |
449 | */ |
450 | private function makeHeadlineElement( string $text ): string { |
451 | return Html::element( |
452 | $this->including() ? 'h3' : 'h2', |
453 | [], |
454 | $text |
455 | ); |
456 | } |
457 | |
458 | /** |
459 | * @inheritDoc |
460 | */ |
461 | public function execute( $subPage ) { |
462 | parent::execute( $subPage ); |
463 | if ( $this->handleAction( $subPage ) ) { |
464 | return; |
465 | } |
466 | |
467 | $out = $this->getOutput(); |
468 | // We only need OOUI (ButtonWidget) when can manage mentors |
469 | // Avoid access to the global context when transcluding (T346760) |
470 | if ( $this->canManageMentors() ) { |
471 | $out->enableOOUI(); |
472 | } |
473 | $out->addHTML( implode( "\n", [ |
474 | $this->makePreHTML(), |
475 | $this->makeHeadlineElement( $this->msg( 'growthexperiments-manage-mentors-auto-assigned' )->text() ), |
476 | Html::element( 'p', [], |
477 | $this->msg( 'growthexperiments-manage-mentors-auto-assigned-text' )->text() |
478 | ), |
479 | $this->getMentorsTable( $this->mentorProvider->getAutoAssignedMentors() ), |
480 | $this->makeHeadlineElement( $this->msg( 'growthexperiments-manage-mentors-manually-assigned' )->text() ), |
481 | Html::element( 'p', [], |
482 | $this->msg( 'growthexperiments-manage-mentors-manually-assigned-text' )->text() |
483 | ), |
484 | $this->getMentorsTable( $this->mentorProvider->getManuallyAssignedMentors() ), |
485 | ] ) ); |
486 | } |
487 | } |