Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
56.43% |
79 / 140 |
|
50.00% |
4 / 8 |
CRAP | |
0.00% |
0 / 1 |
ChangeMentor | |
56.43% |
79 / 140 |
|
50.00% |
4 / 8 |
92.85 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
wasMentorChanged | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
log | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
42 | |||
validate | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
4 | |||
notify | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
20 | |||
isMentorshipEnabledForUser | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
execute | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
7 | |||
getMenteeUser | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Mentorship; |
4 | |
5 | use GrowthExperiments\HelpPanel; |
6 | use GrowthExperiments\HomepageHooks; |
7 | use GrowthExperiments\Mentorship\Store\MentorStore; |
8 | use ManualLogEntry; |
9 | use MediaWiki\Deferred\DeferredUpdates; |
10 | use MediaWiki\Extension\Notifications\Model\Event; |
11 | use MediaWiki\Registration\ExtensionRegistry; |
12 | use MediaWiki\Status\Status; |
13 | use MediaWiki\Title\Title; |
14 | use MediaWiki\User\User; |
15 | use MediaWiki\User\UserFactory; |
16 | use MediaWiki\User\UserIdentity; |
17 | use Psr\Log\LoggerInterface; |
18 | use Wikimedia\Rdbms\IConnectionProvider; |
19 | |
20 | class ChangeMentor { |
21 | private UserIdentity $mentee; |
22 | private ?UserIdentity $mentor; |
23 | private ?UserIdentity $newMentor; |
24 | private UserIdentity $performer; |
25 | private LoggerInterface $logger; |
26 | private MentorManager $mentorManager; |
27 | private MentorStore $mentorStore; |
28 | private UserFactory $userFactory; |
29 | private IConnectionProvider $connectionProvider; |
30 | private ?User $menteeUser = null; |
31 | |
32 | /** |
33 | * @param UserIdentity $mentee Mentee's user object |
34 | * @param UserIdentity $performer Performer's user object |
35 | * @param LoggerInterface $logger |
36 | * @param Mentor|null $mentor Old mentor |
37 | * @param MentorManager $mentorManager |
38 | * @param MentorStore $mentorStore |
39 | * @param UserFactory $userFactory |
40 | * @param IConnectionProvider $connectionProvider |
41 | */ |
42 | public function __construct( |
43 | UserIdentity $mentee, |
44 | UserIdentity $performer, |
45 | LoggerInterface $logger, |
46 | ?Mentor $mentor, |
47 | MentorManager $mentorManager, |
48 | MentorStore $mentorStore, |
49 | UserFactory $userFactory, |
50 | IConnectionProvider $connectionProvider |
51 | ) { |
52 | $this->logger = $logger; |
53 | |
54 | $this->performer = $performer; |
55 | $this->mentee = $mentee; |
56 | $this->mentorManager = $mentorManager; |
57 | $this->mentorStore = $mentorStore; |
58 | $this->userFactory = $userFactory; |
59 | $this->connectionProvider = $connectionProvider; |
60 | $this->mentor = $mentor ? $mentor->getUserIdentity() : null; |
61 | } |
62 | |
63 | /** |
64 | * Was mentee's mentor already changed? |
65 | * |
66 | * @return bool |
67 | */ |
68 | public function wasMentorChanged(): bool { |
69 | $res = $this->connectionProvider->getReplicaDatabase()->newSelectQueryBuilder() |
70 | ->select( [ 'log_page' ] ) |
71 | ->from( 'logging' ) |
72 | ->where( [ |
73 | 'log_type' => 'growthexperiments', |
74 | 'log_namespace' => NS_USER, |
75 | 'log_title' => Title::makeTitle( NS_USER, $this->mentee->getName() )->getDbKey() |
76 | ] ) |
77 | ->caller( __METHOD__ ) |
78 | ->fetchRow(); |
79 | return (bool)$res; |
80 | } |
81 | |
82 | /** |
83 | * Log mentor change |
84 | * |
85 | * @param string $reason Reason for the change |
86 | * @param bool $forceBot Whether to mark this log entry as bot-made |
87 | */ |
88 | protected function log( string $reason, bool $forceBot ) { |
89 | $this->logger->debug( |
90 | 'Logging mentor change for {mentee} from {oldMentor} to {newMentor} by {performer}', [ |
91 | 'mentee' => $this->mentee, |
92 | 'oldMentor' => $this->mentor, |
93 | 'newMentor' => $this->newMentor, |
94 | 'performer' => $this->performer, |
95 | ] ); |
96 | |
97 | if ( $this->performer->getId() === $this->newMentor->getId() ) { |
98 | $primaryLogtype = 'claimmentee'; |
99 | } else { |
100 | $primaryLogtype = 'setmentor'; |
101 | } |
102 | $logEntry = new ManualLogEntry( |
103 | 'growthexperiments', |
104 | $this->mentor ? |
105 | $primaryLogtype : |
106 | "$primaryLogtype-no-previous-mentor" |
107 | ); |
108 | $logEntry->setPerformer( $this->performer ); |
109 | $logEntry->setTarget( $this->getMenteeUser()->getUserPage() ); |
110 | $logEntry->setComment( $reason ); |
111 | if ( $forceBot ) { |
112 | // Don't spam RecentChanges with bulk changes (T304428) |
113 | $logEntry->setForceBotFlag( true ); |
114 | } |
115 | $parameters = []; |
116 | if ( $this->mentor ) { |
117 | // $this->mentor is null when no mentor existed previously |
118 | $parameters['4::previous-mentor'] = $this->mentor->getName(); |
119 | } |
120 | if ( $this->performer->getId() !== $this->newMentor->getId() ) { |
121 | $parameters['5::new-mentor'] = $this->newMentor->getName(); |
122 | } |
123 | $logEntry->setParameters( $parameters ); |
124 | $logid = $logEntry->insert(); |
125 | $logEntry->publish( $logid ); |
126 | } |
127 | |
128 | /** |
129 | * Verify the mentor change is possible |
130 | * |
131 | * @return Status |
132 | */ |
133 | private function validate(): Status { |
134 | $this->logger->debug( |
135 | 'Validating mentor change for {mentee} from {oldMentor} to {newMentor}', [ |
136 | 'mentee' => $this->mentee, |
137 | 'oldMentor' => $this->mentor, |
138 | 'newMentor' => $this->newMentor |
139 | ] ); |
140 | $status = Status::newGood(); |
141 | |
142 | if ( !$this->getMenteeUser()->isNamed() ) { |
143 | $this->logger->info( |
144 | 'Mentor change for {mentee} from {oldMentor} to {newMentor}' |
145 | . ' did not succeed, because the mentee doesn\'t exist', [ |
146 | 'mentee' => $this->mentee, |
147 | 'oldMentor' => $this->mentor, |
148 | 'newMentor' => $this->newMentor |
149 | ] ); |
150 | $status->fatal( 'growthexperiments-homepage-claimmentee-no-user' ); |
151 | return $status; |
152 | } |
153 | if ( $this->mentor && $this->mentor->equals( $this->newMentor ) ) { |
154 | $this->logger->info( |
155 | 'Mentor change for {mentee} from {oldMentor} to {newMentor}' |
156 | . ' did not succeed, because the old and new mentor are equal', [ |
157 | 'mentee' => $this->mentee, |
158 | 'oldMentor' => $this->mentor, |
159 | 'newMentor' => $this->newMentor |
160 | ] ); |
161 | $status->fatal( |
162 | 'growthexperiments-homepage-claimmentee-already-mentor', |
163 | $this->mentee->getName(), |
164 | $this->performer->getName() |
165 | ); |
166 | return $status; |
167 | } |
168 | |
169 | return $status; |
170 | } |
171 | |
172 | /** |
173 | * Notify mentee about the mentor change |
174 | * |
175 | * @param string $reason Reason for the change |
176 | */ |
177 | protected function notify( string $reason ) { |
178 | if ( ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) ) { |
179 | DeferredUpdates::addCallableUpdate( function () use ( $reason ) { |
180 | $this->logger->debug( 'Notify {mentee} about mentor change done by {performer}', [ |
181 | 'mentee' => $this->mentee, |
182 | 'performer' => $this->performer |
183 | ] ); |
184 | Event::create( [ |
185 | 'type' => 'mentor-changed', |
186 | 'title' => $this->getMenteeUser()->getUserPage(), |
187 | 'extra' => [ |
188 | 'mentee' => $this->mentee->getId(), |
189 | 'oldMentor' => $this->mentor->getName(), |
190 | ], |
191 | 'agent' => $this->newMentor, |
192 | ] ); |
193 | |
194 | if ( |
195 | $this->performer->equals( $this->newMentor ) && |
196 | $this->mentor !== null |
197 | ) { |
198 | // mentee was claimed, notify old mentor as well |
199 | Event::create( [ |
200 | 'type' => 'mentee-claimed', |
201 | 'title' => $this->getMenteeUser()->getUserPage(), |
202 | 'extra' => [ |
203 | 'mentor' => $this->mentor->getId(), |
204 | 'reason' => $reason |
205 | ], |
206 | 'agent' => $this->performer |
207 | ] ); |
208 | } |
209 | } ); |
210 | } |
211 | } |
212 | |
213 | /** |
214 | * Does user have mentorship-consuming features enabled? |
215 | * |
216 | * This is used to skip notifications about mentorship changes when the user doesn't actually |
217 | * have relevant features enabled. |
218 | * |
219 | * @note This is a separate method to make unit testing possible |
220 | * (HomepageHooks::isHomepageEnabled and HelpPanel::shouldShowHelpPanelToUser both use global |
221 | * state) |
222 | * @param UserIdentity $user |
223 | * @return bool |
224 | */ |
225 | protected function isMentorshipEnabledForUser( UserIdentity $user ): bool { |
226 | return HomepageHooks::isHomepageEnabled( $user ) |
227 | || HelpPanel::shouldShowHelpPanelToUser( $user ); |
228 | } |
229 | |
230 | /** |
231 | * Change mentor |
232 | * |
233 | * This sets the new primary mentor in the database and adds a log under Special:Log. In most |
234 | * cases, it also notifies the mentee about the mentor change. Notification is only |
235 | * sent if all of the following conditions are true: |
236 | * |
237 | * 1) Mentee has access to Special:Homepage or Help panel |
238 | * 2) Mentee has mentorship enabled (MENTORSHIP_ENABLED) |
239 | * |
240 | * If mentee's mentorship state is MENTORSHIP_DISABLED, access to mentorship is enabled by |
241 | * this method, except when $bulkChange is true, but a notification is not sent. |
242 | * |
243 | * @param UserIdentity $newMentor New mentor to assign |
244 | * @param string $reason Reason for the change |
245 | * @param bool $bulkChange Is this a part of a bulk mentor reassignment (used by |
246 | * ReassignMentees class) |
247 | * @return Status |
248 | */ |
249 | public function execute( |
250 | UserIdentity $newMentor, |
251 | string $reason, |
252 | bool $bulkChange = false |
253 | ): Status { |
254 | // Ensure mentor/mentee relationship is dropped if the mentee is opted out from mentorship (T354259) |
255 | if ( $this->mentorManager->getMentorshipStateForUser( $this->mentee ) === |
256 | MentorManager::MENTORSHIP_OPTED_OUT ) { |
257 | $this->logger->info( |
258 | 'ChangeMentor dropped mentee relationship for {user} ' |
259 | . 'because the user is opted out of mentorship', |
260 | [ 'user' => $this->mentee->getName() ] |
261 | ); |
262 | $this->mentorStore->dropMenteeRelationship( $this->mentee ); |
263 | |
264 | // Pretend the action failed, which is likely better than pretending it succeeded |
265 | // (leaving the on-wiki user wondering "why is there no log in |
266 | // Special:Log/growthexperiments for the reassignment"). We might've performed |
267 | // an internal cleanup by dropping the relationship, but from the on-wiki users point |
268 | // of view, the mentor change failed. |
269 | return Status::newFatal( |
270 | 'growthexperiments-homepage-claimmentee-opt-out', |
271 | $this->mentee->getName() |
272 | ); |
273 | } |
274 | |
275 | $this->newMentor = $newMentor; |
276 | $status = $this->validate(); |
277 | if ( !$status->isOK() ) { |
278 | return $status; |
279 | } |
280 | |
281 | $this->mentorStore->setMentorForUser( $this->mentee, $this->newMentor, MentorStore::ROLE_PRIMARY ); |
282 | $this->log( $reason, $bulkChange ); |
283 | |
284 | if ( $this->isMentorshipEnabledForUser( $this->mentee ) ) { |
285 | $mentorshipState = $this->mentorManager->getMentorshipStateForUser( $this->mentee ); |
286 | |
287 | if ( $mentorshipState === MentorManager::MENTORSHIP_ENABLED ) { |
288 | $this->notify( $reason ); |
289 | } |
290 | |
291 | if ( !$bulkChange && $mentorshipState === MentorManager::MENTORSHIP_DISABLED ) { |
292 | // For non-bulk changes when MENTORSHIP_DISABLED (=GrowthExperiments decided not |
293 | // to include the mentorship module), set the state to MENTORSHIP_ENABLED to ensure |
294 | // the user can benefit from mentorship (T327206). |
295 | // NOTE: Do not enable for MENTORSHIP_OPTOUT, as that means "I'm not interested |
296 | // in being mentored at all" (as an explicit user choice). |
297 | // NOTE: Call to notify() is intentionally above this condition. For users who |
298 | // didn't have mentorship access before, notification "Your mentor has changed" |
299 | // would be confusing (T330035). |
300 | |
301 | $this->mentorManager->setMentorshipStateForUser( |
302 | $this->mentee, |
303 | MentorManager::MENTORSHIP_ENABLED |
304 | ); |
305 | } |
306 | } |
307 | |
308 | return $status; |
309 | } |
310 | |
311 | private function getMenteeUser(): User { |
312 | if ( !$this->menteeUser ) { |
313 | $this->menteeUser = $this->userFactory->newFromUserIdentity( $this->mentee ); |
314 | } |
315 | return $this->menteeUser; |
316 | } |
317 | } |