Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 99 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
MentorFilterHooks | |
0.00% |
0 / 99 |
|
0.00% |
0 / 5 |
240 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onChangesListSpecialPageStructuredFilters | |
0.00% |
0 / 60 |
|
0.00% |
0 / 1 |
72 | |||
getStarredMenteeIds | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getUnstarredMenteeIds | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
convertUserIdsToActorIds | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Mentorship\Hooks; |
4 | |
5 | use ChangesListStringOptionsFilter; |
6 | use ChangesListStringOptionsFilterGroup; |
7 | use GrowthExperiments\MentorDashboard\MenteeOverview\StarredMenteesStore; |
8 | use GrowthExperiments\Mentorship\Provider\MentorProvider; |
9 | use GrowthExperiments\Mentorship\Store\MentorStore; |
10 | use GrowthExperiments\WikiConfigException; |
11 | use MediaWiki\Config\Config; |
12 | use MediaWiki\Context\IContextSource; |
13 | use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageStructuredFiltersHook; |
14 | use MediaWiki\User\UserIdentity; |
15 | use RecentChange; |
16 | use Wikimedia\ObjectCache\HashBagOStuff; |
17 | use Wikimedia\Rdbms\IDatabase; |
18 | |
19 | /** |
20 | * RecentChanges filters for mentors. Separate from MentorHooks because MentorManager |
21 | * depends on the session so making more common hooks depend on it would break ResourceLoader. |
22 | */ |
23 | class MentorFilterHooks implements ChangesListSpecialPageStructuredFiltersHook { |
24 | |
25 | /** @var Config */ |
26 | private $config; |
27 | |
28 | /** @var MentorStore */ |
29 | private $mentorStore; |
30 | |
31 | /** @var StarredMenteesStore */ |
32 | private $starredMenteesStore; |
33 | |
34 | /** @var MentorProvider */ |
35 | private $mentorProvider; |
36 | |
37 | /** @var HashBagOStuff Mentor [starred|unstarred]:username => UserIdentity[] list of mentees */ |
38 | private $menteeCache; |
39 | |
40 | /** |
41 | * @param Config $config |
42 | * @param MentorStore $mentorStore |
43 | * @param StarredMenteesStore $starredMenteesStore |
44 | * @param MentorProvider $mentorProvider |
45 | */ |
46 | public function __construct( |
47 | Config $config, |
48 | MentorStore $mentorStore, |
49 | StarredMenteesStore $starredMenteesStore, |
50 | MentorProvider $mentorProvider |
51 | ) { |
52 | $this->config = $config; |
53 | $this->mentorStore = $mentorStore; |
54 | $this->starredMenteesStore = $starredMenteesStore; |
55 | $this->mentorProvider = $mentorProvider; |
56 | $this->menteeCache = new HashBagOStuff(); |
57 | } |
58 | |
59 | /** @inheritDoc */ |
60 | public function onChangesListSpecialPageStructuredFilters( $special ) { |
61 | // Somewhat arbitrarily, use the dashboard feature flag to expose the mentor filters. |
62 | // Also make sure the user is actually a mentor. |
63 | try { |
64 | if ( !$this->config->get( 'GEMentorDashboardEnabled' ) |
65 | || !$this->mentorProvider->isMentor( $special->getUser() ) |
66 | ) { |
67 | return; |
68 | } |
69 | } catch ( WikiConfigException $wikiConfigException ) { |
70 | return; |
71 | } |
72 | |
73 | $group = new ChangesListStringOptionsFilterGroup( [ |
74 | 'name' => 'mentorship', |
75 | 'title' => 'growthexperiments-rcfilters-mentorship-title', |
76 | 'isFullCoverage' => false, |
77 | 'filters' => [], |
78 | 'default' => '', |
79 | 'queryCallable' => function ( |
80 | string $specialPageClassName, |
81 | IContextSource $context, |
82 | IDatabase $dbr, |
83 | array &$tables, |
84 | array &$fields, |
85 | array &$conds, |
86 | array &$query_options, |
87 | array &$join_conds, |
88 | array $selectedValues |
89 | ) { |
90 | if ( !$selectedValues ) { |
91 | return; |
92 | } |
93 | |
94 | $targetUserIds = []; |
95 | if ( in_array( 'starred', $selectedValues, true ) ) { |
96 | $targetUserIds = $this->getStarredMenteeIds( $context->getUser() ); |
97 | } |
98 | if ( in_array( 'unstarred', $selectedValues, true ) ) { |
99 | $targetUserIds = array_merge( $targetUserIds, $this->getUnstarredMenteeIds( $context->getUser() ) ); |
100 | } |
101 | $targetActorIds = $this->convertUserIdsToActorIds( $dbr, $targetUserIds ); |
102 | |
103 | // The query is shared with other hook handlers, so with the associative array format |
104 | // there is a risk of key conflict. Convert into non-associate instead. |
105 | // Only apply when $targetIds has at least one ID |
106 | if ( $targetActorIds !== [] ) { |
107 | $conds['rc_actor'] = $targetActorIds; |
108 | } else { |
109 | $conds[] = '0=1'; |
110 | } |
111 | }, |
112 | ] ); |
113 | $special->registerFilterGroup( $group ); |
114 | |
115 | $starredMenteesFilter = new ChangesListStringOptionsFilter( [ |
116 | 'name' => 'starred', |
117 | 'group' => $group, |
118 | 'label' => 'growthexperiments-rcfilters-mentorship-starred-label', |
119 | 'description' => 'growthexperiments-rcfilters-mentorship-starred-desc', |
120 | 'priority' => 0, |
121 | 'cssClassSuffix' => 'starred-mentee', |
122 | 'isRowApplicableCallable' => function ( IContextSource $context, RecentChange $rc ) { |
123 | $starredMenteeIds = $this->getStarredMenteeIds( $context->getUser() ); |
124 | return in_array( $rc->getPerformerIdentity()->getId(), $starredMenteeIds, true ); |
125 | }, |
126 | ] ); |
127 | $unstarredMenteesFilter = new ChangesListStringOptionsFilter( [ |
128 | 'name' => 'unstarred', |
129 | 'group' => $group, |
130 | 'label' => 'growthexperiments-rcfilters-mentorship-unstarred-label', |
131 | 'description' => 'growthexperiments-rcfilters-mentorship-unstarred-desc', |
132 | 'priority' => -1, |
133 | 'cssClassSuffix' => 'unstarred-mentee', |
134 | 'isRowApplicableCallable' => function ( IContextSource $context, RecentChange $rc ) { |
135 | $unstarredMenteeIds = $this->getUnstarredMenteeIds( $context->getUser() ); |
136 | return in_array( $rc->getPerformerIdentity()->getId(), $unstarredMenteeIds, true ); |
137 | }, |
138 | ] ); |
139 | } |
140 | |
141 | /** |
142 | * Helper method to load the current user's starred mentees, with caching. |
143 | * @param UserIdentity $user |
144 | * @return int[] |
145 | */ |
146 | private function getStarredMenteeIds( UserIdentity $user ): array { |
147 | $key = $this->menteeCache->makeKey( 'starred', $user->getId() ); |
148 | if ( $this->menteeCache->hasKey( $key ) ) { |
149 | return $this->menteeCache->get( $key ); |
150 | } |
151 | |
152 | $starredMentees = $this->starredMenteesStore->getStarredMentees( $user ); |
153 | $starredMenteeIds = array_map( static function ( UserIdentity $user ) { |
154 | return $user->getId(); |
155 | }, $starredMentees ); |
156 | |
157 | $this->menteeCache->set( $key, $starredMenteeIds ); |
158 | return $starredMenteeIds; |
159 | } |
160 | |
161 | /** |
162 | * Helper method to load the current user's unstarred mentees, with caching. |
163 | * @param UserIdentity $user |
164 | * @return int[] |
165 | */ |
166 | private function getUnstarredMenteeIds( UserIdentity $user ): array { |
167 | $key = $this->menteeCache->makeKey( 'unstarred', $user->getId() ); |
168 | if ( $this->menteeCache->hasKey( $key ) ) { |
169 | return $this->menteeCache->get( $key ); |
170 | } |
171 | |
172 | $mentees = $this->mentorStore->getMenteesByMentor( |
173 | $user, |
174 | MentorStore::ROLE_PRIMARY, |
175 | false, |
176 | false |
177 | ); |
178 | $menteeIds = array_map( static function ( UserIdentity $user ) { |
179 | return $user->getId(); |
180 | }, $mentees ); |
181 | $starredMenteeIds = $this->getStarredMenteeIds( $user ); |
182 | $unstarredMenteeIds = array_diff( $menteeIds, $starredMenteeIds ); |
183 | |
184 | $this->menteeCache->set( $key, $unstarredMenteeIds ); |
185 | return $unstarredMenteeIds; |
186 | } |
187 | |
188 | /** |
189 | * @param IDatabase $db |
190 | * @param int[] $userIds |
191 | * @return int[] |
192 | */ |
193 | private function convertUserIdsToActorIds( IDatabase $db, array $userIds ) { |
194 | if ( !$userIds ) { |
195 | return []; |
196 | } |
197 | |
198 | // No need to worry about properly acquiring actor IDs - if it shows up in |
199 | // recent changes, it already has an actor ID |
200 | $res = $db->newSelectQueryBuilder() |
201 | ->select( 'actor_id' ) |
202 | ->from( 'actor' ) |
203 | ->where( [ 'actor_user' => $userIds ] ) |
204 | ->caller( __METHOD__ ) |
205 | ->fetchFieldValues(); |
206 | return array_map( 'intval', $res ); |
207 | } |
208 | |
209 | } |