Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
36.36% covered (danger)
36.36%
28 / 77
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReassignMentees
36.36% covered (danger)
36.36%
28 / 77
25.00% covered (danger)
25.00%
1 / 4
49.11
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getStage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 reassignMentees
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 doReassignMentees
44.23% covered (danger)
44.23%
23 / 52
0.00% covered (danger)
0.00%
0 / 1
12.24
1<?php
2
3namespace GrowthExperiments\Mentorship;
4
5use GrowthExperiments\Mentorship\Provider\MentorProvider;
6use GrowthExperiments\Mentorship\Store\MentorStore;
7use GrowthExperiments\WikiConfigException;
8use MediaWiki\JobQueue\JobQueueGroupFactory;
9use MediaWiki\Status\StatusFormatter;
10use MediaWiki\User\UserIdentity;
11use MediaWiki\WikiMap\WikiMap;
12use MessageLocalizer;
13use Psr\Log\LoggerAwareTrait;
14use Psr\Log\NullLogger;
15use Wikimedia\Rdbms\IDatabase;
16
17class ReassignMentees {
18    use LoggerAwareTrait;
19
20    public const STAGE_LISTED_AS_MENTOR = 1;
21    public const STAGE_NOT_LISTED_HAS_MENTEES = 2;
22    public const STAGE_NOT_LISTED_NO_MENTEES = 3;
23
24    private IDatabase $dbw;
25    private MentorManager $mentorManager;
26    private MentorProvider $mentorProvider;
27    private MentorStore $mentorStore;
28    private ChangeMentorFactory $changeMentorFactory;
29    private JobQueueGroupFactory $jobQueueGroupFactory;
30    private StatusFormatter $statusFormatter;
31    private UserIdentity $performer;
32    private UserIdentity $mentor;
33    private MessageLocalizer $messageLocalizer;
34
35    /**
36     * @param IDatabase $dbw
37     * @param MentorManager $mentorManager
38     * @param MentorProvider $mentorProvider
39     * @param MentorStore $mentorStore
40     * @param ChangeMentorFactory $changeMentorFactory
41     * @param JobQueueGroupFactory $jobQueueGroupFactory
42     * @param StatusFormatter $statusFormatter
43     * @param UserIdentity $performer
44     * @param UserIdentity $mentor
45     * @param MessageLocalizer $messageLocalizer
46     */
47    public function __construct(
48        IDatabase $dbw,
49        MentorManager $mentorManager,
50        MentorProvider $mentorProvider,
51        MentorStore $mentorStore,
52        ChangeMentorFactory $changeMentorFactory,
53        JobQueueGroupFactory $jobQueueGroupFactory,
54        StatusFormatter $statusFormatter,
55        UserIdentity $performer,
56        UserIdentity $mentor,
57        MessageLocalizer $messageLocalizer
58    ) {
59        $this->dbw = $dbw;
60        $this->mentorManager = $mentorManager;
61        $this->mentorProvider = $mentorProvider;
62        $this->mentorStore = $mentorStore;
63        $this->changeMentorFactory = $changeMentorFactory;
64        $this->jobQueueGroupFactory = $jobQueueGroupFactory;
65        $this->statusFormatter = $statusFormatter;
66        $this->performer = $performer;
67        $this->mentor = $mentor;
68        $this->messageLocalizer = $messageLocalizer;
69        $this->logger = new NullLogger();
70    }
71
72    /**
73     * @return int One of ReassignMentees::STAGE_* constants
74     */
75    public function getStage(): int {
76        if ( $this->mentorProvider->isMentor( $this->mentor ) ) {
77            return self::STAGE_LISTED_AS_MENTOR;
78        } elseif ( $this->mentorStore->hasAnyMentees( $this->mentor, MentorStore::ROLE_PRIMARY ) ) {
79            return self::STAGE_NOT_LISTED_HAS_MENTEES;
80        } else {
81            return self::STAGE_NOT_LISTED_NO_MENTEES;
82        }
83    }
84
85    /**
86     * Reassign mentees currently assigned to the mentor via a job
87     *
88     * If no job is needed, use doReassignMentees directly.
89     *
90     * @param string $reassignMessageKey Message key used in in ChangeMentor notification; needs
91     * to accept one parameter (username of the previous mentor). Additional parameters can be
92     * passed via $reassignMessageAdditionalParams.
93     * @param mixed ...$reassignMessageAdditionalParams
94     */
95    public function reassignMentees(
96        string $reassignMessageKey,
97        ...$reassignMessageAdditionalParams
98    ): void {
99        // checking if any mentees exist is a cheap operation; do not submit a job if it is going
100        // to be a no-op.
101        if ( $this->mentorStore->hasAnyMentees( $this->mentor, MentorStore::ROLE_PRIMARY ) ) {
102            $this->jobQueueGroupFactory->makeJobQueueGroup()->lazyPush(
103                new ReassignMenteesJob( [
104                    'mentorId' => $this->mentor->getId(),
105                    'performerId' => $this->performer->getId(),
106                    'reassignMessageKey' => $reassignMessageKey,
107                    'reassignMessageAdditionalParams' => $reassignMessageAdditionalParams,
108                ] )
109            );
110        }
111    }
112
113    /**
114     * Actually reassign all mentees currently assigned to the mentor
115     *
116     * @param string $reassignMessageKey Message key used in in ChangeMentor notification; needs
117     * to accept one parameter (username of the previous mentor). Additional parameters can be
118     * passed via $reassignMessageAdditionalParams.
119     * @param mixed ...$reassignMessageAdditionalParams
120     * @return bool True if successful, false otherwise.
121     */
122    public function doReassignMentees(
123        string $reassignMessageKey,
124        ...$reassignMessageAdditionalParams
125    ): bool {
126        $lockName = 'GrowthExperiments-ReassignMentees-' . $this->mentor->getId() .
127            WikiMap::getCurrentWikiId();
128        if ( !$this->dbw->lock( $lockName, __METHOD__, 0 ) ) {
129            $this->logger->warning(
130                __METHOD__ . ' failed to acquire a lock'
131            );
132            return false;
133        }
134
135        // only process primary mentors (T309984). Backup mentors will be automatically ignored by
136        // MentorPageMentorManager::getMentorForUser and replaced with a valid mentor if needed
137        $mentees = $this->mentorStore->getMenteesByMentor( $this->mentor, MentorStore::ROLE_PRIMARY );
138        foreach ( $mentees as $mentee ) {
139            $changeMentor = $this->changeMentorFactory->newChangeMentor(
140                $mentee,
141                $this->performer
142            );
143
144            try {
145                $newMentor = $this->mentorManager->getRandomAutoAssignedMentor( $mentee );
146            } catch ( WikiConfigException $e ) {
147                $this->logger->warning(
148                    'ReassignMentees failed to reassign mentees for {mentor}; mentor list is invalid',
149                    [
150                        'mentor' => $this->mentor->getName()
151                    ]
152                );
153                return false;
154            }
155
156            if ( !$newMentor ) {
157                $this->logger->warning(
158                    'ReassignMentees failed to reassign mentees for {mentor}; no mentor is available',
159                    [
160                        'mentor' => $this->mentor->getName(),
161                        'impact' => 'Mentor-mentee relationship dropped'
162                    ]
163                );
164                $this->mentorStore->dropMenteeRelationship( $mentee );
165                continue;
166            }
167
168            $status = $changeMentor->execute(
169                $newMentor,
170                $this->messageLocalizer->msg(
171                    $reassignMessageKey,
172                    $this->mentor->getName(),
173                    ...$reassignMessageAdditionalParams
174                )->text(),
175                true
176            );
177            if ( !$status->isOK() ) {
178                $this->logger->warning(
179                    'ReassignMentees failed to assign {mentor} as {user}\'s mentor for {reason}',
180                    [
181                        'mentor' => $newMentor->getName(),
182                        'user' => $mentee->getName(),
183                        'reason' => $this->statusFormatter->getWikiText( $status, [ 'lang' => 'en' ] )
184                    ]
185                );
186            }
187        }
188
189        $this->dbw->unlock( $lockName, __METHOD__ );
190        return true;
191    }
192}