Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 110 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
SpecialInvitationList | |
0.00% |
0 / 110 |
|
0.00% |
0 / 7 |
462 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
maybeDisplayList | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
30 | |||
displayList | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
20 | |||
getWorklistLinks | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
splitUsersByScore | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getUserLinks | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents\Special; |
6 | |
7 | use LogicException; |
8 | use MediaWiki\Extension\CampaignEvents\Invitation\InvitationList; |
9 | use MediaWiki\Extension\CampaignEvents\Invitation\InvitationListNotFoundException; |
10 | use MediaWiki\Extension\CampaignEvents\Invitation\InvitationListStore; |
11 | use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsCentralUserLookup; |
12 | use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUser; |
13 | use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUserNotFoundException; |
14 | use MediaWiki\Extension\CampaignEvents\MWEntity\HiddenCentralUserException; |
15 | use MediaWiki\Extension\CampaignEvents\MWEntity\MWAuthorityProxy; |
16 | use MediaWiki\Extension\CampaignEvents\MWEntity\UserLinker; |
17 | use MediaWiki\Extension\CampaignEvents\Permissions\PermissionChecker; |
18 | use MediaWiki\Html\Html; |
19 | use MediaWiki\Html\TemplateParser; |
20 | use MediaWiki\Message\Message; |
21 | use MediaWiki\Page\PageIdentity; |
22 | use MediaWiki\SpecialPage\SpecialPage; |
23 | use OOUI\HtmlSnippet; |
24 | use OOUI\MessageWidget; |
25 | |
26 | class SpecialInvitationList extends SpecialPage { |
27 | use InvitationFeatureAccessTrait; |
28 | |
29 | public const PAGE_NAME = 'InvitationList'; |
30 | |
31 | private PermissionChecker $permissionChecker; |
32 | private InvitationListStore $invitationListStore; |
33 | private CampaignsCentralUserLookup $centralUserLookup; |
34 | private UserLinker $userLinker; |
35 | private TemplateParser $templateParser; |
36 | |
37 | private const HIGHLY_RECOMMENDED_MIN_SCORE = 70; |
38 | public const RECOMMENDED_MIN_SCORE = 25; |
39 | |
40 | public function __construct( |
41 | PermissionChecker $permissionChecker, |
42 | InvitationListStore $invitationListStore, |
43 | CampaignsCentralUserLookup $centralUserLookup, |
44 | UserLinker $userLinker |
45 | ) { |
46 | parent::__construct( self::PAGE_NAME ); |
47 | $this->permissionChecker = $permissionChecker; |
48 | $this->invitationListStore = $invitationListStore; |
49 | $this->centralUserLookup = $centralUserLookup; |
50 | $this->userLinker = $userLinker; |
51 | $this->templateParser = new TemplateParser( __DIR__ . '/../../templates' ); |
52 | } |
53 | |
54 | /** |
55 | * @inheritDoc |
56 | */ |
57 | public function execute( $par ): void { |
58 | $this->setHeaders(); |
59 | $this->outputHeader(); |
60 | $mwAuthority = new MWAuthorityProxy( $this->getAuthority() ); |
61 | $out = $this->getOutput(); |
62 | $out->enableOOUI(); |
63 | $isEnabledAndPermitted = $this->checkInvitationFeatureAccess( |
64 | $this->getOutput(), |
65 | $mwAuthority |
66 | ); |
67 | if ( $isEnabledAndPermitted ) { |
68 | $this->maybeDisplayList( $par ); |
69 | } |
70 | } |
71 | |
72 | private function maybeDisplayList( ?string $par ): void { |
73 | if ( $par === null ) { |
74 | $this->getOutput()->redirect( |
75 | SpecialPage::getTitleFor( SpecialMyInvitationLists::PAGE_NAME )->getLocalURL() |
76 | ); |
77 | return; |
78 | } |
79 | |
80 | $listID = (int)$par; |
81 | if ( (string)$listID !== $par ) { |
82 | $this->getOutput()->addHTML( Html::errorBox( |
83 | $this->msg( 'campaignevents-invitation-list-invalid-id' )->parseAsBlock() |
84 | ) ); |
85 | return; |
86 | } |
87 | try { |
88 | $invitationList = $this->invitationListStore->getInvitationList( $listID ); |
89 | } catch ( InvitationListNotFoundException $_ ) { |
90 | $this->getOutput()->addHTML( Html::errorBox( |
91 | $this->msg( 'campaignevents-invitation-list-does-not-exist' )->parseAsBlock() |
92 | ) ); |
93 | return; |
94 | } |
95 | |
96 | $user = $this->centralUserLookup->newFromAuthority( new MWAuthorityProxy( $this->getAuthority() ) ); |
97 | if ( !$invitationList->getCreator()->equals( $user ) ) { |
98 | $this->getOutput()->addHTML( Html::errorBox( |
99 | $this->msg( 'campaignevents-invitation-list-not-creator' )->parseAsBlock() |
100 | ) ); |
101 | return; |
102 | } |
103 | |
104 | $this->displayList( $invitationList ); |
105 | } |
106 | |
107 | private function displayList( InvitationList $list ): void { |
108 | $out = $this->getOutput(); |
109 | $out->setPageTitleMsg( |
110 | $this->msg( 'campaignevents-invitationlist-event' )->params( $list->getName() ) |
111 | ); |
112 | |
113 | if ( $list->getStatus() === InvitationList::STATUS_PENDING ) { |
114 | $messageWidget = new MessageWidget( [ |
115 | 'type' => 'notice', |
116 | 'label' => new HtmlSnippet( $this->msg( 'campaignevents-invitation-list-processing' )->parse() ) |
117 | ] ); |
118 | $out->addHTML( $messageWidget ); |
119 | return; |
120 | } |
121 | |
122 | // TODO: Load only the styles for accordions. We need a RL module for that. |
123 | $out->addModuleStyles( [ 'codex-styles' ] ); |
124 | |
125 | $invitationListUsers = $this->invitationListStore->getInvitationListUsers( $list->getListID() ); |
126 | [ $highlyRecommended, $recommended ] = self::splitUsersByScore( $invitationListUsers ); |
127 | $numEditors = count( $highlyRecommended ) + count( $recommended ); |
128 | |
129 | $out->addWikiMsg( |
130 | 'campaignevents-invitation-list-intro', |
131 | Message::numParam( $numEditors ) |
132 | ); |
133 | |
134 | $noUsersWarning = ''; |
135 | if ( $numEditors > 0 ) { |
136 | $allUsersByID = array_fill_keys( array_merge( $highlyRecommended, $recommended ), null ); |
137 | // Warm up the cache for all users, even those that don't exist or are deleted. |
138 | $allUsernames = $this->centralUserLookup->getNamesIncludingDeletedAndSuppressed( $allUsersByID ); |
139 | // But preload links only for those who actually exist. |
140 | $usernamesToPreload = array_filter( |
141 | $allUsernames, |
142 | static function ( $name ) { |
143 | return $name !== CampaignsCentralUserLookup::USER_HIDDEN && |
144 | $name !== CampaignsCentralUserLookup::USER_NOT_FOUND; |
145 | } |
146 | ); |
147 | $this->userLinker->preloadUserLinks( $usernamesToPreload ); |
148 | } else { |
149 | $noUsersWarning = new MessageWidget( [ |
150 | 'type' => 'warning', |
151 | 'label' => new HtmlSnippet( $this->msg( 'campaignevents-invitationlist-no-editors' )->parse() ), |
152 | ] ); |
153 | } |
154 | |
155 | $data = [ |
156 | 'noUsersWarning' => $noUsersWarning, |
157 | 'highlyRecommendedLabel' => $this->msg( 'campaignevents-invitationlist-highly-recommended' )->text(), |
158 | 'highlyRecommendedDesc' => $this->msg( 'campaignevents-invitationlist-highly-recommended-info' )->text(), |
159 | 'highlyRecommended' => $this->getUserLinks( $highlyRecommended ), |
160 | 'recommendedLabel' => $this->msg( 'campaignevents-invitationlist-recommended' )->text(), |
161 | 'recommendedDesc' => $this->msg( 'campaignevents-invitationlist-recommended-info' )->text(), |
162 | 'recommended' => $this->getUserLinks( $recommended ), |
163 | 'worklistLabel' => $this->msg( 'campaignevents-invitationlist-worklist-label' )->text(), |
164 | 'worklist' => $this->getWorklistLinks( $list->getListID() ) |
165 | ]; |
166 | |
167 | $template = $this->templateParser->processTemplate( 'InvitationList', $data ); |
168 | $out->addHTML( $template ); |
169 | } |
170 | |
171 | private function getWorklistLinks( int $invitationListID ): array { |
172 | $worklist = $this->invitationListStore->getWorklist( $invitationListID ); |
173 | $pagesByWiki = $worklist->getPagesByWiki(); |
174 | if ( count( $pagesByWiki ) !== 1 ) { |
175 | throw new LogicException( 'Expected a single wiki' ); |
176 | } |
177 | $localPages = reset( $pagesByWiki ); |
178 | $linkRenderer = $this->getLinkRenderer(); |
179 | return array_map( static fn ( PageIdentity $page ) => $linkRenderer->makeLink( $page ), $localPages ); |
180 | } |
181 | |
182 | /** |
183 | * Given a list of potential invitees, group them by score range. |
184 | * |
185 | * @param array<int,int> $users |
186 | * @return int[][] First element is a list of highly recommended user IDs. Second element is a list of recommended |
187 | * user IDs. |
188 | * @phan-return array{0:list<int>,1:list<int>} |
189 | */ |
190 | private static function splitUsersByScore( array $users ): array { |
191 | $highlyRecommended = []; |
192 | $recommended = []; |
193 | foreach ( $users as $userID => $score ) { |
194 | if ( $score >= self::HIGHLY_RECOMMENDED_MIN_SCORE ) { |
195 | $highlyRecommended[] = $userID; |
196 | } elseif ( $score >= self::RECOMMENDED_MIN_SCORE ) { |
197 | $recommended[] = $userID; |
198 | } |
199 | } |
200 | |
201 | return [ $highlyRecommended, $recommended ]; |
202 | } |
203 | |
204 | /** |
205 | * @param int[] $userIDs |
206 | * @return string[] |
207 | */ |
208 | private function getUserLinks( array $userIDs ): array { |
209 | $links = []; |
210 | foreach ( $userIDs as $userID ) { |
211 | try { |
212 | $links[] = $this->userLinker->generateUserLink( new CentralUser( $userID ) ); |
213 | } catch ( CentralUserNotFoundException | HiddenCentralUserException $_ ) { |
214 | continue; |
215 | } |
216 | } |
217 | return $links; |
218 | } |
219 | } |