Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
28.57% |
32 / 112 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
SpecialMentorDashboard | |
28.57% |
32 / 112 |
|
0.00% |
0 / 14 |
252.77 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
generatePageviewToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getModules | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getModuleGroups | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
maybeRedirectToEnrollAsMentor | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
requireMentorDashboardEnabled | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
requireMentorList | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
execute | |
96.97% |
32 / 33 |
|
0.00% |
0 / 1 |
4 | |||
maybeLogVisit | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
maybeSetSeenPreference | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
isEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
displayRestrictionError | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Specials; |
4 | |
5 | use ErrorPageError; |
6 | use GrowthExperiments\DashboardModule\IDashboardModule; |
7 | use GrowthExperiments\EventLogging\SpecialMentorDashboardLogger; |
8 | use GrowthExperiments\MentorDashboard\MentorDashboardDiscoveryHooks; |
9 | use GrowthExperiments\MentorDashboard\MentorDashboardModuleRegistry; |
10 | use GrowthExperiments\Mentorship\Provider\MentorProvider; |
11 | use GrowthExperiments\Util; |
12 | use MediaWiki\Deferred\DeferredUpdates; |
13 | use MediaWiki\Html\Html; |
14 | use MediaWiki\JobQueue\JobQueueGroupFactory; |
15 | use MediaWiki\Registration\ExtensionRegistry; |
16 | use MediaWiki\SpecialPage\SpecialPage; |
17 | use MediaWiki\User\Options\UserOptionsLookup; |
18 | use MWCryptRand; |
19 | use PermissionsError; |
20 | use UserOptionsUpdateJob; |
21 | |
22 | class 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 | } |