Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
6.17% covered (danger)
6.17%
15 / 243
3.85% covered (danger)
3.85%
1 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
Mentorship
6.17% covered (danger)
6.17%
15 / 243
3.85% covered (danger)
3.85%
1 / 26
1361.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getMentorLastActive
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 getHeaderText
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 buildSection
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getEllipsisWidget
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getHeaderIconName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBody
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getMobileSummaryBody
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getFooter
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getModuleStyles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getModules
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getJsConfigVars
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 getActionData
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 canRender
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getMentorUsernameElement
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
6
 getMentorInfo
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getEditCount
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getLastActive
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getIntroText
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 getQuestionButton
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 getMentor
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getRecentQuestionsSection
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getRecentQuestions
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getAboutMentorshipElement
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 getUserGender
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMentorGender
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace GrowthExperiments\HomepageModules;
4
5use DateInterval;
6use GenderCache;
7use GrowthExperiments\ExperimentUserManager;
8use GrowthExperiments\HelpPanel;
9use GrowthExperiments\HelpPanel\QuestionRecord;
10use GrowthExperiments\HelpPanel\QuestionStoreFactory;
11use GrowthExperiments\MentorDashboard\MentorTools\MentorStatusManager;
12use GrowthExperiments\Mentorship\MentorManager;
13use GrowthExperiments\Mentorship\Provider\MentorProvider;
14use IContextSource;
15use MediaWiki\Config\Config;
16use MediaWiki\Config\ConfigException;
17use MediaWiki\Html\Html;
18use MediaWiki\MediaWikiServices;
19use MediaWiki\User\User;
20use MediaWiki\User\UserIdentity;
21use MediaWiki\Utils\MWTimestamp;
22use MessageLocalizer;
23use OOUI\ButtonWidget;
24use OOUI\IconWidget;
25
26/**
27 * This is the "Mentorship" module. It shows your mentor and
28 * provides ways to interact with them.
29 *
30 * @package GrowthExperiments\HomepageModules
31 */
32class Mentorship extends BaseModule {
33
34    public const MENTORSHIP_MODULE_QUESTION_TAG = 'mentorship module question';
35    public const MENTORSHIP_HELPPANEL_QUESTION_TAG = 'mentorship panel question';
36    public const QUESTION_PREF = 'growthexperiments-mentor-questions';
37
38    /** @var UserIdentity */
39    private $mentor;
40
41    /** @var QuestionRecord[] */
42    private $recentQuestions = [];
43
44    /** @var MentorManager */
45    private $mentorManager;
46
47    /** @var MentorStatusManager */
48    private $mentorStatusManager;
49
50    /** @var GenderCache */
51    private $genderCache;
52
53    /**
54     * @param IContextSource $context
55     * @param Config $wikiConfig
56     * @param ExperimentUserManager $experimentUserManager
57     * @param MentorManager $mentorManager
58     * @param MentorStatusManager $mentorStatusManager
59     * @param GenderCache $genderCache
60     */
61    public function __construct(
62        IContextSource $context,
63        Config $wikiConfig,
64        ExperimentUserManager $experimentUserManager,
65        MentorManager $mentorManager,
66        MentorStatusManager $mentorStatusManager,
67        GenderCache $genderCache
68    ) {
69        parent::__construct( 'mentorship', $context, $wikiConfig, $experimentUserManager );
70        $this->mentorManager = $mentorManager;
71        $this->mentorStatusManager = $mentorStatusManager;
72        $this->genderCache = $genderCache;
73    }
74
75    /**
76     * Get the time a mentor was last active, as a human-readable relative time.
77     * @param UserIdentity $mentor The mentoring user.
78     * @param User $mentee The mentored user (for time formatting).
79     * @param MessageLocalizer $messageLocalizer
80     * @return string
81     */
82    public static function getMentorLastActive(
83        UserIdentity $mentor, User $mentee, MessageLocalizer $messageLocalizer
84    ) {
85        $editTimestamp = new MWTimestamp( MediaWikiServices::getInstance()->getUserEditTracker()
86            ->getLatestEditTimestamp( $mentor ) );
87        $editTimestamp->offsetForUser( $mentee );
88        $editDate = $editTimestamp->format( 'Ymd' );
89
90        $now = new MWTimestamp();
91        $now->offsetForUser( $mentee );
92        $timeDiff = $now->diff( $editTimestamp );
93
94        $today = $now->format( 'Ymd' );
95        $yesterday = $now->timestamp->sub( new DateInterval( 'P1D' ) )->format( 'Ymd' );
96
97        if ( $editDate === $today ) {
98            $text = $messageLocalizer
99                ->msg( 'growthexperiments-homepage-mentorship-mentor-active-today' )
100                ->params( $mentor->getName() )
101                ->text();
102        } elseif ( $editDate === $yesterday ) {
103            $text = $messageLocalizer
104                ->msg( 'growthexperiments-homepage-mentorship-mentor-active-yesterday' )
105                ->params( $mentor->getName() )
106                ->text();
107        } else {
108            $text = $messageLocalizer
109                ->msg( 'growthexperiments-homepage-mentorship-mentor-active-days-ago' )
110                ->params( $mentor->getName() )
111                ->numParams( $timeDiff->days )
112                ->text();
113        }
114        return $text;
115    }
116
117    /**
118     * @inheritDoc
119     */
120    protected function getHeaderText() {
121        return $this->getContext()
122            ->msg( 'growthexperiments-homepage-mentorship-header' )
123            ->params( $this->getContext()->getUser()->getName() )
124            ->params( $this->getMentor()->getName() )
125            ->text();
126    }
127
128    /**
129     * @inheritDoc
130     */
131    protected function buildSection( $name, $content, $tag = 'div' ) {
132        if ( $name === 'header' && $this->getMode() === self::RENDER_DESKTOP ) {
133            return Html::rawElement(
134                'div',
135                [ 'class' => 'growthexperiments-homepage-mentorship-header-wrapper' ],
136                implode( "\n", [
137                    parent::buildSection( $name, $content, $tag ),
138                    $this->getEllipsisWidget()
139                ] )
140            );
141        }
142        return parent::buildSection( $name, $content, $tag );
143    }
144
145    /**
146     * @return string
147     */
148    private function getEllipsisWidget() {
149        // NOTE: This will be replaced with ButtonMenuSelectWidget in EllipsisMenu.js on the
150        // client side.
151        return Html::rawElement(
152            'div',
153            [ 'class' => 'growthexperiments-homepage-mentorship-ellipsis' ],
154            new ButtonWidget( [
155                'id' => 'mw-ge-homepage-mentorship-ellipsis',
156                'icon' => 'ellipsis',
157                'framed' => false,
158                'invisibleLabel' => true,
159                'infusable' => true,
160            ] )
161        );
162    }
163
164    /**
165     * @inheritDoc
166     */
167    protected function getHeaderIconName() {
168        return 'userTalk';
169    }
170
171    /**
172     * @inheritDoc
173     */
174    protected function getBody() {
175        return implode( "\n", [
176            $this->getAboutMentorshipElement(),
177            $this->getMentorUsernameElement( true ),
178            $this->getMentorInfo(),
179            $this->getIntroText(),
180            $this->getQuestionButton(),
181            $this->getRecentQuestionsSection(),
182        ] );
183    }
184
185    /**
186     * @inheritDoc
187     */
188    protected function getMobileSummaryBody() {
189        return implode( "\n", [
190            Html::element( 'p', [], $this->msg(
191                'growthexperiments-homepage-mentorship-preintro',
192                $this->getMentor()->getName()
193            )->text() ),
194            $this->getMentorUsernameElement( false ),
195            $this->getLastActive(),
196        ] );
197    }
198
199    /**
200     * @inheritDoc
201     */
202    protected function getFooter() {
203        return Html::element(
204            'a',
205            [
206                'href' => User::newFromIdentity( $this->getMentor() )->getTalkPage()->getLinkURL(),
207                'data-link-id' => 'mentor-usertalk',
208            ],
209            $this->getContext()
210                ->msg( 'growthexperiments-homepage-mentorship-mentor-conversations' )
211                ->params( $this->getMentor()->getName() )
212                ->params( $this->getContext()->getUser()->getName() )
213                ->text()
214        );
215    }
216
217    /**
218     * @inheritDoc
219     */
220    protected function getModuleStyles() {
221        return array_merge(
222            parent::getModuleStyles(),
223            [ 'oojs-ui.styles.icons-user' ]
224        );
225    }
226
227    /**
228     * @inheritDoc
229     */
230    protected function getModules() {
231        return $this->getMode() !== self::RENDER_MOBILE_SUMMARY ?
232            [ 'ext.growthExperiments.Homepage.Mentorship' ] : [];
233    }
234
235    /**
236     * @inheritDoc
237     */
238    protected function getJsConfigVars() {
239        $mentor = $this->getMentor();
240        $effectiveMentor = $this->mentorManager->getEffectiveMentorForUserSafe(
241            $this->getUser()
242        )->getUserIdentity();
243        $mentorBackTimestamp = $this->mentorStatusManager->getMentorBackTimestamp( $mentor );
244        return [
245            'GEHomepageMentorshipMentorName' => $mentor->getName(),
246            'GEHomepageMentorshipMentorGender' => $this->getMentorGender(),
247            'GEHomepageMentorshipEffectiveMentorName' => $effectiveMentor->getName(),
248            'GEHomepageMentorshipEffectiveMentorGender' => $this->getUserGender( $effectiveMentor ),
249            'GEHomepageMentorshipBackAt' => $mentorBackTimestamp ? $this->getContext()->getLanguage()->date(
250                $mentorBackTimestamp
251            ) : null,
252        ] + HelpPanel::getUserEmailConfigVars( $this->getContext()->getUser() );
253    }
254
255    /**
256     * @inheritDoc
257     */
258    protected function getActionData() {
259        $editTracker = MediaWikiServices::getInstance()->getUserEditTracker();
260        $archivedQuestions = 0;
261        $unarchivedQuestions = 0;
262        foreach ( $this->getRecentQuestions() as $questionRecord ) {
263            if ( $questionRecord->isArchived() ) {
264                $archivedQuestions++;
265            } else {
266                $unarchivedQuestions++;
267            }
268        }
269
270        return array_merge(
271            parent::getActionData(),
272            [
273                'mentorEditCount' => $editTracker->getUserEditCount( $this->getMentor() ),
274                'mentorLastActive' => $editTracker->getLatestEditTimestamp( $this->getMentor() ),
275                'archivedQuestions' => $archivedQuestions,
276                'unarchivedQuestions' => $unarchivedQuestions
277            ]
278        );
279    }
280
281    /**
282     * @inheritDoc
283     */
284    protected function canRender() {
285        return $this->mentorManager->getMentorshipStateForUser(
286            $this->getUser()
287        ) === MentorManager::MENTORSHIP_ENABLED &&
288            $this->mentorManager->getEffectiveMentorForUserSafe( $this->getUser() ) !== null;
289    }
290
291    private function getMentorUsernameElement( $link ) {
292        $iconElement = new IconWidget( [ 'icon' => 'mentor' ] );
293        $usernameElement = Html::element(
294            'span',
295            [ 'class' => 'growthexperiments-homepage-mentorship-username' ],
296            $this->getContext()->getLanguage()->embedBidi(
297                $this->getMentor()->getName()
298            )
299        );
300        if ( $link ) {
301            $content = Html::rawElement(
302                'a',
303                [
304                    'href' => User::newFromIdentity( $this->getMentor() )->getUserPage()->getLinkURL(),
305                    'data-link-id' => 'mentor-userpage',
306                    'class' => 'growthexperiments-homepage-mentorship-userlink-link'
307                ],
308                $iconElement . $usernameElement
309            );
310        } else {
311            $content = Html::rawElement(
312                'span',
313                [],
314                $iconElement . $usernameElement
315            );
316        }
317        return Html::rawElement( 'div', [
318            'class' => 'growthexperiments-homepage-mentorship-userlink'
319        ], $content );
320    }
321
322    private function getMentorInfo() {
323        return Html::rawElement(
324            'div',
325            [
326                'class' => 'growthexperiments-homepage-mentorship-mentorinfo'
327            ],
328            $this->getEditCount() . ' &bull; ' . $this->getLastActive()
329        );
330    }
331
332    private function getEditCount() {
333        $text = $this->getContext()
334            ->msg( 'growthexperiments-homepage-mentorship-mentor-edits' )
335            ->numParams( MediaWikiServices::getInstance()->getUserEditTracker()
336                ->getUserEditCount( $this->getMentor() ) )
337            ->text();
338        return Html::element( 'span', [
339            'class' => 'growthexperiments-homepage-mentorship-editcount'
340        ], $text );
341    }
342
343    private function getLastActive() {
344        $text = self::getMentorLastActive( $this->getMentor(), $this->getContext()->getUser(),
345            $this->getContext() );
346        return Html::element( 'span', [
347            'class' => 'growthexperiments-homepage-mentorship-lastactive'
348        ], $text );
349    }
350
351    private function getIntroText() {
352        $mentor = $this->mentorManager->getMentorForUser( $this->getContext()->getUser() );
353
354        $introText = $this->getContext()->getLanguage()->truncateForVisual(
355            $mentor->getIntroText(),
356            MentorProvider::INTRO_TEXT_LENGTH
357        );
358        if ( $mentor->hasCustomIntroText() ) {
359            $introText = $this->msg( 'quotation-marks' )
360                ->inContentLanguage()
361                ->params( $introText )
362                ->text();
363        }
364
365        return Html::element(
366            'div',
367            [ 'class' => 'growthexperiments-homepage-mentorship-intro' ],
368            $introText
369        );
370    }
371
372    private function getQuestionButton() {
373        return new ButtonWidget( [
374            'id' => 'mw-ge-homepage-mentorship-cta',
375            'classes' => [ 'growthexperiments-homepage-mentorship-cta' ],
376            'active' => false,
377            'label' => $this->getContext()
378                ->msg( 'growthexperiments-homepage-mentorship-question-button' )
379                ->params( $this->getMentor()->getName() )
380                ->params( $this->getContext()->getUser()->getName() )
381                ->text(),
382            // nojs action
383            'href' => User::newFromIdentity( $this->getMentor() )->getTalkPage()->getLinkURL( [
384                'action' => 'edit',
385                'section' => 'new',
386            ] ),
387            'infusable' => true,
388        ] );
389    }
390
391    /**
392     * @return UserIdentity|false The current user's mentor or false if not set.
393     * @throws ConfigException
394     */
395    private function getMentor() {
396        if ( !$this->mentor ) {
397            $mentor = $this->mentorManager->getMentorForUserSafe( $this->getContext()->getUser() );
398            if ( $mentor ) {
399                $this->mentor = $mentor->getUserIdentity();
400            } else {
401                return false;
402            }
403        }
404        return $this->mentor;
405    }
406
407    private function getRecentQuestionsSection() {
408        $recentQuestionFormatter = new RecentQuestionsFormatter(
409            $this->getContext(),
410            $this->getRecentQuestions(),
411            self::QUESTION_PREF
412        );
413        return $recentQuestionFormatter->format();
414    }
415
416    private function getRecentQuestions() {
417        if ( count( $this->recentQuestions ) ) {
418            return $this->recentQuestions;
419        }
420        $this->recentQuestions = QuestionStoreFactory::newFromContextAndStorage(
421            $this->getContext(),
422            self::QUESTION_PREF
423        )->loadQuestions();
424        return $this->recentQuestions;
425    }
426
427    private function getAboutMentorshipElement() {
428        return Html::rawElement(
429            'p',
430            [ 'class' => 'growthexperiments-homepage-mentorship-about' ],
431            implode( "\n", [
432                Html::element(
433                    'span',
434                    [],
435                    $this->msg(
436                        'growthexperiments-homepage-mentorship-preintro',
437                        $this->getMentor()->getName()
438                    )->text()
439                ),
440                Html::element(
441                    'a',
442                    [
443                        'id' => 'growthexperiments-homepage-mentorship-learn-more',
444                        'href' => '#',
445                    ],
446                    $this->msg( 'growthexperiments-homepage-mentorship-learn-more' )->text()
447                )
448            ] )
449        );
450    }
451
452    /**
453     * Get the gender of the specified user
454     *
455     * @param UserIdentity $user
456     * @return string
457     */
458    private function getUserGender( UserIdentity $user ): string {
459        return $this->genderCache->getGenderOf( $user, __METHOD__ );
460    }
461
462    /**
463     * Get the gender of the mentor
464     *
465     * @return string
466     */
467    private function getMentorGender(): string {
468        return $this->getUserGender( $this->getMentor() );
469    }
470
471}