Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
28.57% covered (danger)
28.57%
32 / 112
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMentorDashboard
28.57% covered (danger)
28.57%
32 / 112
0.00% covered (danger)
0.00%
0 / 14
252.77
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescription
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 generatePageviewToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getModules
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getModuleGroups
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 maybeRedirectToEnrollAsMentor
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 requireMentorDashboardEnabled
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 requireMentorList
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 execute
96.97% covered (success)
96.97%
32 / 33
0.00% covered (danger)
0.00%
0 / 1
4
 maybeLogVisit
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 maybeSetSeenPreference
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 isEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 displayRestrictionError
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace GrowthExperiments\Specials;
4
5use ErrorPageError;
6use ExtensionRegistry;
7use GrowthExperiments\DashboardModule\IDashboardModule;
8use GrowthExperiments\EventLogging\SpecialMentorDashboardLogger;
9use GrowthExperiments\MentorDashboard\MentorDashboardDiscoveryHooks;
10use GrowthExperiments\MentorDashboard\MentorDashboardModuleRegistry;
11use GrowthExperiments\Mentorship\Provider\MentorProvider;
12use GrowthExperiments\Util;
13use MediaWiki\Deferred\DeferredUpdates;
14use MediaWiki\Html\Html;
15use MediaWiki\JobQueue\JobQueueGroupFactory;
16use MediaWiki\SpecialPage\SpecialPage;
17use MediaWiki\User\Options\UserOptionsLookup;
18use MWCryptRand;
19use PermissionsError;
20use UserOptionsUpdateJob;
21
22class SpecialMentorDashboard extends SpecialPage {
23
24    /**
25     * @var string Unique identifier for this specific rendering of Special:Homepage.
26     * Used by various EventLogging schemas to correlate events.
27     */
28    private string $pageviewToken;
29    private MentorDashboardModuleRegistry $mentorDashboardModuleRegistry;
30    private MentorProvider $mentorProvider;
31    private UserOptionsLookup $userOptionsLookup;
32    private JobQueueGroupFactory $jobQueueGroupFactory;
33
34    /**
35     * @param MentorDashboardModuleRegistry $mentorDashboardModuleRegistry
36     * @param MentorProvider $mentorProvider
37     * @param UserOptionsLookup $userOptionsLookup
38     * @param JobQueueGroupFactory $jobQueueGroupFactory
39     */
40    public function __construct(
41        MentorDashboardModuleRegistry $mentorDashboardModuleRegistry,
42        MentorProvider $mentorProvider,
43        UserOptionsLookup $userOptionsLookup,
44        JobQueueGroupFactory $jobQueueGroupFactory
45    ) {
46        parent::__construct( 'MentorDashboard' );
47
48        $this->pageviewToken = $this->generatePageviewToken();
49        $this->mentorDashboardModuleRegistry = $mentorDashboardModuleRegistry;
50        $this->mentorProvider = $mentorProvider;
51        $this->userOptionsLookup = $userOptionsLookup;
52        $this->jobQueueGroupFactory = $jobQueueGroupFactory;
53    }
54
55    /** @inheritDoc */
56    protected function getGroupName() {
57        return 'growth-tools';
58    }
59
60    /**
61     * @inheritDoc
62     */
63    public function getDescription() {
64        return $this->msg( 'growthexperiments-mentor-dashboard-title' );
65    }
66
67    /**
68     * Returns 32-character random string.
69     * The token is used for client-side logging and can be retrieved on Special:MentorDashboard via
70     * the wgGEMentorDashboardPageviewToken JS variable.
71     * @return string
72     */
73    private function generatePageviewToken() {
74        return \Wikimedia\base_convert( MWCryptRand::generateHex( 40 ), 16, 32, 32 );
75    }
76
77    /**
78     * @param bool $isMobile
79     * @return IDashboardModule[]
80     */
81    private function getModules( bool $isMobile = false ): array {
82        $enabledModules = $this->getConfig()->get( 'GEMentorDashboardEnabledModules' );
83        $modules = [];
84        foreach ( $enabledModules as $moduleId => $enabled ) {
85            if ( !$enabled ) {
86                continue;
87            }
88
89            $modules[$moduleId] = $this->mentorDashboardModuleRegistry->get(
90                $moduleId,
91                $this->getContext()
92            );
93        }
94        return $modules;
95    }
96
97    /**
98     * @return string[][]
99     */
100    private function getModuleGroups(): array {
101        return [
102            'main' => [
103                'mentee-overview'
104            ],
105            'sidebar' => [
106                'personalized-praise',
107                'mentor-tools',
108                'resources'
109            ]
110        ];
111    }
112
113    /**
114     * Check whether the user is a mentor and redirect to
115     * Special:EnrollAsMentor if they're not AND structured mentor
116     * list is used.
117     */
118    private function maybeRedirectToEnrollAsMentor(): void {
119        if ( !$this->mentorProvider->isMentor( $this->getUser() ) ) {
120            $this->getOutput()->redirect(
121                SpecialPage::getTitleFor( 'EnrollAsMentor' )->getLocalURL()
122            );
123        }
124    }
125
126    /**
127     * Ensure mentor dashboard is enabled
128     *
129     * @throws ErrorPageError
130     */
131    private function requireMentorDashboardEnabled() {
132        if ( !$this->isEnabled() ) {
133            // Mentor dashboard is disabled, display a meaningful restriction error
134            throw new ErrorPageError(
135                'growthexperiments-mentor-dashboard-title',
136                'growthexperiments-mentor-dashboard-disabled'
137            );
138        }
139    }
140
141    /**
142     * Ensure the automatic mentor list is configured
143     *
144     * @throws ErrorPageError if mentor list is missing
145     */
146    private function requireMentorList() {
147        if ( !$this->mentorProvider->getSignupTitle() ) {
148            throw new ErrorPageError(
149                'growthexperiments-mentor-dashboard-title',
150                'growthexperiments-mentor-dashboard-misconfigured-missing-list'
151            );
152        }
153    }
154
155    /**
156     * @inheritDoc
157     */
158    public function execute( $par ) {
159        $this->requireNamedUser();
160        $this->maybeRedirectToEnrollAsMentor();
161        $this->requireMentorDashboardEnabled();
162        $this->requireMentorList();
163
164        parent::execute( $par );
165
166        $out = $this->getContext()->getOutput();
167        $out->addJsConfigVars( [
168            'wgGEMentorDashboardPageviewToken' => $this->pageviewToken
169        ] );
170
171        $out->enableOOUI();
172        $dashboardModules = [ 'ext.growthExperiments.MentorDashboard' ];
173
174        $out->addModules( $dashboardModules );
175        $out->addModuleStyles( 'ext.growthExperiments.MentorDashboard.styles' );
176
177        $out->addHTML( Html::openElement( 'div', [
178            'class' => 'growthexperiments-mentor-dashboard-container'
179        ] ) );
180
181        $modules = $this->getModules( false );
182
183        foreach ( $this->getModuleGroups() as $group => $moduleNames ) {
184            $out->addHTML( Html::openElement(
185                'div',
186                [
187                    'class' => "growthexperiments-mentor-dashboard-group-$group"
188                ]
189            ) );
190
191            foreach ( $moduleNames as $moduleName ) {
192                $module = $modules[$moduleName] ?? null;
193                if ( !$module ) {
194                    continue;
195                }
196                $out->addHTML( $module->render( IDashboardModule::RENDER_DESKTOP ) );
197            }
198
199            $out->addHTML( Html::closeElement( 'div' ) );
200        }
201
202        $out->addHTML( Html::closeElement( 'div' ) );
203
204        $this->maybeLogVisit();
205        $this->maybeSetSeenPreference();
206    }
207
208    /**
209     * Log visit to the mentor dashboard, if EventLogging is installed
210     */
211    private function maybeLogVisit(): void {
212        if ( ExtensionRegistry::getInstance()->isLoaded( 'EventLogging' ) ) {
213            DeferredUpdates::addCallableUpdate( function () {
214                $logger = new SpecialMentorDashboardLogger(
215                    $this->pageviewToken,
216                    $this->getUser(),
217                    $this->getRequest(),
218                    Util::isMobile( $this->getSkin() )
219                );
220                $logger->log();
221            } );
222        }
223    }
224
225    /**
226     * If applicable, record that the user seen the dashboard
227     *
228     * This is used by MentorDashboardDiscoveryHooks to decide whether or not
229     * to add a blue dot informing the mentors about their dashboard.
230     *
231     * Happens via a DeferredUpdate, because it doesn't affect what the user
232     * sees in their dashboard (and is not time-sensitive as it depends on a job).
233     */
234    private function maybeSetSeenPreference(): void {
235        DeferredUpdates::addCallableUpdate( function () {
236            $user = $this->getUser();
237            if ( $this->userOptionsLookup->getBoolOption(
238                $user,
239                MentorDashboardDiscoveryHooks::MENTOR_DASHBOARD_SEEN_PREF
240            ) ) {
241                // no need to set the option again
242                return;
243            }
244
245            // we're in a GET context, set the seen pref via a job rather than directly
246            $this->jobQueueGroupFactory->makeJobQueueGroup()->lazyPush( new UserOptionsUpdateJob( [
247                'userId' => $user->getId(),
248                'options' => [
249                    MentorDashboardDiscoveryHooks::MENTOR_DASHBOARD_SEEN_PREF => 1
250                ]
251            ] ) );
252        } );
253    }
254
255    /**
256     * Check if mentor dashboard is enabled via GEMentorDashboardEnabled
257     *
258     * @return bool
259     */
260    private function isEnabled(): bool {
261        return $this->getConfig()->get( 'GEMentorDashboardEnabled' );
262    }
263
264    /**
265     * @inheritDoc
266     */
267    public function displayRestrictionError() {
268        $signupTitle = $this->mentorProvider->getSignupTitle();
269
270        if ( $signupTitle === null ) {
271            throw new PermissionsError(
272                null,
273                [ 'growthexperiments-homepage-mentors-list-missing-or-misconfigured-generic' ]
274            );
275        }
276
277        throw new PermissionsError(
278            null,
279            [ [ 'growthexperiments-mentor-dashboard-must-be-mentor',
280                $signupTitle->getPrefixedText() ] ]
281        );
282    }
283}