Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 452 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
EventDetailsParticipantsModule | |
0.00% |
0 / 452 |
|
0.00% |
0 / 14 |
4290 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
createContent | |
0.00% |
0 / 77 |
|
0.00% |
0 / 1 |
240 | |||
getPrimaryHeader | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
30 | |||
getParticipantsTable | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
2 | |||
getEmptyStateElement | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
getSearchBar | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getTableHeaders | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
72 | |||
getParticipantRows | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 | |||
getCurUserParticipantRow | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getParticipantRow | |
0.00% |
0 / 74 |
|
0.00% |
0 / 1 |
110 | |||
addNonPIIParticipantAnswers | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
42 | |||
getQuestionAnswer | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
getFooter | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
30 | |||
getHeaderControls | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents\FrontendModules; |
6 | |
7 | use MediaWiki\Extension\CampaignEvents\Event\ExistingEventRegistration; |
8 | use MediaWiki\Extension\CampaignEvents\Messaging\CampaignsUserMailer; |
9 | use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsCentralUserLookup; |
10 | use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUserNotFoundException; |
11 | use MediaWiki\Extension\CampaignEvents\MWEntity\HiddenCentralUserException; |
12 | use MediaWiki\Extension\CampaignEvents\MWEntity\ICampaignsAuthority; |
13 | use MediaWiki\Extension\CampaignEvents\MWEntity\UserLinker; |
14 | use MediaWiki\Extension\CampaignEvents\MWEntity\UserNotGlobalException; |
15 | use MediaWiki\Extension\CampaignEvents\Participants\Participant; |
16 | use MediaWiki\Extension\CampaignEvents\Participants\ParticipantsStore; |
17 | use MediaWiki\Extension\CampaignEvents\Participants\UnregisterParticipantCommand; |
18 | use MediaWiki\Extension\CampaignEvents\Permissions\PermissionChecker; |
19 | use MediaWiki\Extension\CampaignEvents\Questions\Answer; |
20 | use MediaWiki\Extension\CampaignEvents\Questions\EventQuestionsRegistry; |
21 | use MediaWiki\Language\Language; |
22 | use MediaWiki\Output\OutputPage; |
23 | use MediaWiki\Parser\Sanitizer; |
24 | use MediaWiki\User\UserFactory; |
25 | use MediaWiki\User\UserIdentity; |
26 | use OOUI\ButtonGroupWidget; |
27 | use OOUI\ButtonWidget; |
28 | use OOUI\CheckboxInputWidget; |
29 | use OOUI\FieldLayout; |
30 | use OOUI\HtmlSnippet; |
31 | use OOUI\IconWidget; |
32 | use OOUI\MessageWidget; |
33 | use OOUI\PanelLayout; |
34 | use OOUI\SearchInputWidget; |
35 | use OOUI\Tag; |
36 | use Wikimedia\Message\IMessageFormatterFactory; |
37 | use Wikimedia\Message\ITextFormatter; |
38 | use Wikimedia\Message\MessageValue; |
39 | |
40 | class EventDetailsParticipantsModule { |
41 | |
42 | private const PARTICIPANTS_LIMIT = 20; |
43 | public const MODULE_STYLES = [ |
44 | 'oojs-ui.styles.icons-moderation', |
45 | 'oojs-ui.styles.icons-user', |
46 | ...UserLinker::MODULE_STYLES |
47 | ]; |
48 | |
49 | private UserLinker $userLinker; |
50 | private ParticipantsStore $participantsStore; |
51 | private CampaignsCentralUserLookup $centralUserLookup; |
52 | private PermissionChecker $permissionChecker; |
53 | private UserFactory $userFactory; |
54 | private CampaignsUserMailer $userMailer; |
55 | |
56 | private ITextFormatter $msgFormatter; |
57 | private EventQuestionsRegistry $eventQuestionsRegistry; |
58 | private Language $language; |
59 | private string $statisticsTabUrl; |
60 | private bool $isPastEvent; |
61 | |
62 | /** |
63 | * @param IMessageFormatterFactory $messageFormatterFactory |
64 | * @param UserLinker $userLinker |
65 | * @param ParticipantsStore $participantsStore |
66 | * @param CampaignsCentralUserLookup $centralUserLookup |
67 | * @param PermissionChecker $permissionChecker |
68 | * @param UserFactory $userFactory |
69 | * @param CampaignsUserMailer $userMailer |
70 | * @param EventQuestionsRegistry $eventQuestionsRegistry |
71 | * @param Language $language |
72 | * @param string $statisticsTabUrl |
73 | */ |
74 | public function __construct( |
75 | IMessageFormatterFactory $messageFormatterFactory, |
76 | UserLinker $userLinker, |
77 | ParticipantsStore $participantsStore, |
78 | CampaignsCentralUserLookup $centralUserLookup, |
79 | PermissionChecker $permissionChecker, |
80 | UserFactory $userFactory, |
81 | CampaignsUserMailer $userMailer, |
82 | EventQuestionsRegistry $eventQuestionsRegistry, |
83 | Language $language, |
84 | string $statisticsTabUrl |
85 | ) { |
86 | $this->userLinker = $userLinker; |
87 | $this->participantsStore = $participantsStore; |
88 | $this->centralUserLookup = $centralUserLookup; |
89 | $this->permissionChecker = $permissionChecker; |
90 | $this->userFactory = $userFactory; |
91 | $this->userMailer = $userMailer; |
92 | |
93 | $this->msgFormatter = $messageFormatterFactory->getTextFormatter( $language->getCode() ); |
94 | $this->eventQuestionsRegistry = $eventQuestionsRegistry; |
95 | $this->language = $language; |
96 | $this->statisticsTabUrl = $statisticsTabUrl; |
97 | $this->isPastEvent = false; |
98 | } |
99 | |
100 | /** |
101 | * @param ExistingEventRegistration $event |
102 | * @param UserIdentity $viewingUser |
103 | * @param ICampaignsAuthority $authority |
104 | * @param bool $isOrganizer |
105 | * @param bool $canEmailParticipants |
106 | * @param bool $isLocalWiki |
107 | * @param OutputPage $out |
108 | * @return Tag |
109 | * |
110 | * @note Ideally, this wouldn't use MW-specific classes for l10n, but it's hard-ish to avoid and |
111 | * probably not worth doing. |
112 | */ |
113 | public function createContent( |
114 | ExistingEventRegistration $event, |
115 | UserIdentity $viewingUser, |
116 | ICampaignsAuthority $authority, |
117 | bool $isOrganizer, |
118 | bool $canEmailParticipants, |
119 | bool $isLocalWiki, |
120 | OutputPage $out |
121 | ): Tag { |
122 | $eventID = $event->getID(); |
123 | $this->isPastEvent = $event->isPast(); |
124 | $totalParticipants = $this->participantsStore->getFullParticipantCountForEvent( $eventID ); |
125 | |
126 | $centralUser = null; |
127 | $curUserParticipant = null; |
128 | try { |
129 | $centralUser = $this->centralUserLookup->newFromAuthority( $authority ); |
130 | $curUserParticipant = $this->participantsStore->getEventParticipant( $eventID, $centralUser, true ); |
131 | } catch ( UserNotGlobalException $_ ) { |
132 | } |
133 | |
134 | $showPrivateParticipants = $isLocalWiki && |
135 | $this->permissionChecker->userCanViewPrivateParticipants( $authority, $event ); |
136 | $otherParticipantsNum = $curUserParticipant ? self::PARTICIPANTS_LIMIT - 1 : self::PARTICIPANTS_LIMIT; |
137 | $otherParticipants = $this->participantsStore->getEventParticipants( |
138 | $eventID, |
139 | $otherParticipantsNum, |
140 | null, |
141 | null, |
142 | null, |
143 | $showPrivateParticipants, |
144 | $centralUser ? [ $centralUser->getCentralID() ] : null |
145 | ); |
146 | $lastParticipant = $otherParticipants ? end( $otherParticipants ) : $curUserParticipant; |
147 | $lastParticipantID = $lastParticipant ? $lastParticipant->getParticipantID() : null; |
148 | $canRemoveParticipants = false; |
149 | if ( $isOrganizer && $isLocalWiki ) { |
150 | $canRemoveParticipants = UnregisterParticipantCommand::checkIsUnregistrationAllowed( $event )->isGood(); |
151 | } |
152 | |
153 | $canViewNonPIIParticipantsData = false; |
154 | if ( $isOrganizer && $isLocalWiki ) { |
155 | $canViewNonPIIParticipantsData = $this->permissionChecker->userCanViewNonPIIParticipantsData( |
156 | $authority, $event |
157 | ); |
158 | } |
159 | |
160 | $nonPIIQuestionIDs = $this->eventQuestionsRegistry->getNonPIIQuestionIDs( |
161 | $event->getParticipantQuestions() |
162 | ); |
163 | |
164 | $items = []; |
165 | $items[] = $this->getPrimaryHeader( |
166 | $event, |
167 | $totalParticipants, |
168 | $canRemoveParticipants, |
169 | $canEmailParticipants, |
170 | $canViewNonPIIParticipantsData |
171 | ); |
172 | if ( $totalParticipants ) { |
173 | $items[] = $this->getParticipantsTable( |
174 | $viewingUser, |
175 | $canRemoveParticipants, |
176 | $canEmailParticipants, |
177 | $canViewNonPIIParticipantsData, |
178 | $curUserParticipant, |
179 | $otherParticipants, |
180 | $authority, |
181 | $event, |
182 | $nonPIIQuestionIDs |
183 | ); |
184 | } |
185 | // This is added even if there are participants, because they might be removed from this page. |
186 | $items[] = $this->getEmptyStateElement( $totalParticipants ); |
187 | |
188 | $out->addJsConfigVars( [ |
189 | 'wgCampaignEventsShowParticipantCheckboxes' => $canRemoveParticipants || $canEmailParticipants, |
190 | 'wgCampaignEventsShowPrivateParticipants' => $showPrivateParticipants, |
191 | 'wgCampaignEventsEventDetailsParticipantsTotal' => $totalParticipants, |
192 | 'wgCampaignEventsLastParticipantID' => $lastParticipantID, |
193 | 'wgCampaignEventsCurUserCentralID' => $centralUser ? $centralUser->getCentralID() : null, |
194 | 'wgCampaignEventsViewerHasEmail' => |
195 | $this->userFactory->newFromUserIdentity( $viewingUser )->isEmailConfirmed(), |
196 | 'wgCampaignEventsNonPIIQuestionIDs' => $nonPIIQuestionIDs, |
197 | ] ); |
198 | |
199 | $layout = new PanelLayout( [ |
200 | 'content' => $items, |
201 | 'padded' => false, |
202 | 'framed' => true, |
203 | 'expanded' => false, |
204 | ] ); |
205 | |
206 | $content = ( new Tag( 'div' ) ) |
207 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-panel' ] ) |
208 | ->appendContent( $layout ); |
209 | |
210 | $footer = $this->getFooter( $eventID, $canViewNonPIIParticipantsData, $event, $out ); |
211 | if ( $footer ) { |
212 | $content->appendContent( $footer ); |
213 | } |
214 | |
215 | return $content; |
216 | } |
217 | |
218 | /** |
219 | * @param ExistingEventRegistration $event |
220 | * @param int $totalParticipants |
221 | * @param bool $canRemoveParticipants |
222 | * @param bool $canEmailParticipants |
223 | * @param bool $canViewNonPIIParticipantsData |
224 | * @return Tag |
225 | */ |
226 | private function getPrimaryHeader( |
227 | ExistingEventRegistration $event, |
228 | int $totalParticipants, |
229 | bool $canRemoveParticipants, |
230 | bool $canEmailParticipants, |
231 | bool $canViewNonPIIParticipantsData |
232 | ): Tag { |
233 | $participantCountText = $this->msgFormatter->format( |
234 | MessageValue::new( 'campaignevents-event-details-header-participants' ) |
235 | ->numParams( $totalParticipants ) |
236 | ); |
237 | $participantsCountElement = ( new Tag( 'span' ) ) |
238 | ->appendContent( $participantCountText ) |
239 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-header-participant-count' ] ); |
240 | $participantsElement = ( new Tag( 'div' ) ) |
241 | ->appendContent( $participantsCountElement ) |
242 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-header-participants' ] ); |
243 | if ( |
244 | $canViewNonPIIParticipantsData && |
245 | !$this->isPastEvent && |
246 | $event->getParticipantQuestions() |
247 | ) { |
248 | $questionsHelp = new ButtonWidget( [ |
249 | 'framed' => false, |
250 | 'icon' => 'info', |
251 | 'label' => $this->msgFormatter->format( |
252 | MessageValue::new( 'campaignevents-event-details-header-questions-help' ) |
253 | ), |
254 | 'invisibleLabel' => true, |
255 | 'classes' => [ 'ext-campaignevents-eventdetails-participants-header-questions-help' ] |
256 | ] ); |
257 | $participantsElement->appendContent( $questionsHelp ); |
258 | } |
259 | $headerTitle = ( new Tag( 'div' ) ) |
260 | ->appendContent( $participantsElement ) |
261 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-header-title' ] ); |
262 | $header = ( new Tag( 'div' ) )->addClasses( [ 'ext-campaignevents-eventdetails-participants-header' ] ); |
263 | |
264 | if ( $totalParticipants ) { |
265 | $headerTitle->appendContent( $this->getSearchBar() ); |
266 | $header->appendContent( $headerTitle ); |
267 | $header->appendContent( $this->getHeaderControls( $canRemoveParticipants, $canEmailParticipants ) ); |
268 | } else { |
269 | $header->appendContent( $headerTitle ); |
270 | } |
271 | |
272 | return $header; |
273 | } |
274 | |
275 | /** |
276 | * @param UserIdentity $viewingUser |
277 | * @param bool $canRemoveParticipants |
278 | * @param bool $canEmailParticipants |
279 | * @param bool $canViewNonPIIParticipantsData |
280 | * @param Participant|null $curUserParticipant |
281 | * @param Participant[] $otherParticipants |
282 | * @param ICampaignsAuthority $authority |
283 | * @param ExistingEventRegistration $event |
284 | * @param int[] $nonPIIQuestionIDs |
285 | * @return Tag |
286 | */ |
287 | private function getParticipantsTable( |
288 | UserIdentity $viewingUser, |
289 | bool $canRemoveParticipants, |
290 | bool $canEmailParticipants, |
291 | bool $canViewNonPIIParticipantsData, |
292 | ?Participant $curUserParticipant, |
293 | array $otherParticipants, |
294 | ICampaignsAuthority $authority, |
295 | ExistingEventRegistration $event, |
296 | array $nonPIIQuestionIDs |
297 | ): Tag { |
298 | // Use an outer container for the infinite scrolling |
299 | $container = ( new Tag( 'div' ) ) |
300 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-container' ] ); |
301 | $table = ( new Tag( 'table' ) ) |
302 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-table' ] ); |
303 | |
304 | $table->appendContent( $this->getTableHeaders( |
305 | $canRemoveParticipants, |
306 | $canEmailParticipants, |
307 | $event, |
308 | $authority, |
309 | $nonPIIQuestionIDs, |
310 | $canViewNonPIIParticipantsData |
311 | ) |
312 | ); |
313 | $table->appendContent( $this->getParticipantRows( |
314 | $curUserParticipant, |
315 | $otherParticipants, |
316 | $canRemoveParticipants, |
317 | $canEmailParticipants, |
318 | $viewingUser, |
319 | $nonPIIQuestionIDs, |
320 | $canViewNonPIIParticipantsData |
321 | ) ); |
322 | $container->appendContent( $table ); |
323 | return $container; |
324 | } |
325 | |
326 | /** |
327 | * @param int $totalParticipants |
328 | * @return Tag |
329 | */ |
330 | private function getEmptyStateElement( int $totalParticipants ): Tag { |
331 | $noParticipantsIcon = new IconWidget( [ |
332 | 'icon' => 'userGroup', |
333 | 'classes' => [ 'ext-campaignevents-eventdetails-no-participants-icon' ] |
334 | ] ); |
335 | |
336 | $noParticipantsClasses = [ 'ext-campaignevents-eventdetails-no-participants-state' ]; |
337 | if ( $totalParticipants > 0 ) { |
338 | $noParticipantsClasses[] = 'ext-campaignevents-eventdetails-hide-element'; |
339 | } |
340 | return ( new Tag() )->appendContent( |
341 | $noParticipantsIcon, |
342 | ( new Tag() )->appendContent( |
343 | $this->msgFormatter->format( |
344 | MessageValue::new( 'campaignevents-event-details-no-participants-state' ) |
345 | ) |
346 | )->addClasses( [ 'ext-campaignevents-eventdetails-no-participants-description' ] ) |
347 | )->addClasses( $noParticipantsClasses ); |
348 | } |
349 | |
350 | /** |
351 | * @return Tag |
352 | */ |
353 | private function getSearchBar(): Tag { |
354 | return new SearchInputWidget( [ |
355 | 'placeholder' => $this->msgFormatter->format( |
356 | MessageValue::new( 'campaignevents-event-details-search-participants-placeholder' ) |
357 | ), |
358 | 'infusable' => true, |
359 | 'classes' => [ 'ext-campaignevents-eventdetails-participants-search' ] |
360 | ] ); |
361 | } |
362 | |
363 | /** |
364 | * @param bool $canRemoveParticipants |
365 | * @param bool $canEmailParticipants |
366 | * @param ExistingEventRegistration $event |
367 | * @param ICampaignsAuthority $authority |
368 | * @param array $nonPIIQuestionIDs |
369 | * @param bool $userCanViewNonPIIParticipantsData |
370 | * @return Tag |
371 | */ |
372 | private function getTableHeaders( |
373 | bool $canRemoveParticipants, |
374 | bool $canEmailParticipants, |
375 | ExistingEventRegistration $event, |
376 | ICampaignsAuthority $authority, |
377 | array $nonPIIQuestionIDs, |
378 | bool $userCanViewNonPIIParticipantsData |
379 | ): Tag { |
380 | $container = ( new Tag( 'thead' ) ) |
381 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-table-header' ] ); |
382 | $row = ( new Tag( 'tr' ) ) |
383 | ->addClasses( [ 'ext-campaignevents-details-user-actions-row' ] ); |
384 | |
385 | if ( $canRemoveParticipants ) { |
386 | $selectAllCheckBoxField = new FieldLayout( |
387 | new CheckboxInputWidget( [ |
388 | 'name' => 'event-details-select-all-participants', |
389 | ] ), |
390 | [ |
391 | 'align' => 'inline', |
392 | 'classes' => [ 'ext-campaignevents-event-details-select-all-participant-checkbox-field' ], |
393 | 'infusable' => true, |
394 | 'label' => $this->msgFormatter->format( |
395 | MessageValue::new( 'campaignevents-event-details-select-all' ) |
396 | ), |
397 | 'invisibleLabel' => true |
398 | ] |
399 | ); |
400 | |
401 | $selectAllCell = ( new Tag( 'th' ) ) |
402 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-selectall-checkbox-cell' ] ) |
403 | ->appendContent( $selectAllCheckBoxField ); |
404 | $row->appendContent( $selectAllCell ); |
405 | } |
406 | |
407 | $headings = [ |
408 | [ |
409 | 'message' => $this->msgFormatter->format( |
410 | MessageValue::new( 'campaignevents-event-details-participants' ) |
411 | ), |
412 | 'cssClasses' => [ 'ext-campaignevents-eventdetails-participants-username-cell' ], |
413 | ], |
414 | [ |
415 | 'message' => $this->msgFormatter->format( |
416 | MessageValue::new( 'campaignevents-event-details-time-registered' ) |
417 | ), |
418 | 'cssClasses' => [ 'ext-campaignevents-eventdetails-participants-time-registered-cell' ], |
419 | ], |
420 | ]; |
421 | if ( $canEmailParticipants ) { |
422 | $headings[] = [ |
423 | 'message' => $this->msgFormatter->format( |
424 | MessageValue::new( 'campaignevents-event-details-can-receive-email' ) |
425 | ), |
426 | 'cssClasses' => [ 'ext-campaignevents-eventdetails-participants-can-receive-email-cell' ], |
427 | ]; |
428 | } |
429 | |
430 | if ( !$this->isPastEvent && $userCanViewNonPIIParticipantsData ) { |
431 | $nonPIIQuestionLabels = $this->eventQuestionsRegistry->getNonPIIQuestionLabels( |
432 | $nonPIIQuestionIDs |
433 | ); |
434 | if ( $nonPIIQuestionLabels ) { |
435 | foreach ( $nonPIIQuestionLabels as $nonPIIQuestionLabel ) { |
436 | $headings[] = [ |
437 | 'message' => $this->msgFormatter->format( |
438 | MessageValue::new( $nonPIIQuestionLabel ) |
439 | ), |
440 | 'cssClasses' => [ 'ext-campaignevents-eventdetails-participants-non-pii-question-cells' ], |
441 | ]; |
442 | } |
443 | } |
444 | } |
445 | |
446 | foreach ( $headings as $heading ) { |
447 | $row->appendContent( |
448 | ( new Tag( 'th' ) )->appendContent( $heading[ 'message' ] )->addClasses( $heading[ 'cssClasses' ] ) |
449 | ); |
450 | } |
451 | $container->appendContent( $row ); |
452 | |
453 | return $container; |
454 | } |
455 | |
456 | /** |
457 | * @param Participant|null $curUserParticipant |
458 | * @param Participant[] $otherParticipants |
459 | * @param bool $canRemoveParticipants |
460 | * @param bool $canEmailParticipants |
461 | * @param UserIdentity $viewingUser |
462 | * @param array $nonPIIQuestionIDs |
463 | * @param bool $userCanViewNonPIIParticipantsData |
464 | * @return Tag |
465 | */ |
466 | private function getParticipantRows( |
467 | ?Participant $curUserParticipant, |
468 | array $otherParticipants, |
469 | bool $canRemoveParticipants, |
470 | bool $canEmailParticipants, |
471 | UserIdentity $viewingUser, |
472 | array $nonPIIQuestionIDs, |
473 | bool $userCanViewNonPIIParticipantsData |
474 | ): Tag { |
475 | $body = new Tag( 'tbody' ); |
476 | if ( $curUserParticipant ) { |
477 | $body->appendContent( $this->getCurUserParticipantRow( |
478 | $curUserParticipant, |
479 | $canRemoveParticipants, |
480 | $canEmailParticipants, |
481 | $viewingUser, |
482 | $nonPIIQuestionIDs, |
483 | $userCanViewNonPIIParticipantsData |
484 | ) ); |
485 | } |
486 | |
487 | foreach ( $otherParticipants as $participant ) { |
488 | $body->appendContent( |
489 | $this->getParticipantRow( |
490 | $participant, |
491 | $canRemoveParticipants, |
492 | $canEmailParticipants, |
493 | $viewingUser, |
494 | $nonPIIQuestionIDs, |
495 | $userCanViewNonPIIParticipantsData |
496 | ) |
497 | ); |
498 | } |
499 | return $body; |
500 | } |
501 | |
502 | /** |
503 | * @param Participant $participant |
504 | * @param bool $canRemoveParticipants |
505 | * @param bool $canEmailParticipants |
506 | * @param UserIdentity $viewingUser |
507 | * @param array $nonPIIQuestionIDs |
508 | * @param bool $userCanViewNonPIIParticipantsData |
509 | * @return Tag |
510 | */ |
511 | private function getCurUserParticipantRow( |
512 | Participant $participant, |
513 | bool $canRemoveParticipants, |
514 | bool $canEmailParticipants, |
515 | UserIdentity $viewingUser, |
516 | array $nonPIIQuestionIDs, |
517 | bool $userCanViewNonPIIParticipantsData |
518 | ): Tag { |
519 | $row = $this->getParticipantRow( |
520 | $participant, |
521 | $canRemoveParticipants, |
522 | $canEmailParticipants, |
523 | $viewingUser, |
524 | $nonPIIQuestionIDs, |
525 | $userCanViewNonPIIParticipantsData |
526 | ); |
527 | $row->addClasses( [ 'ext-campaignevents-details-current-user-row' ] ); |
528 | return $row; |
529 | } |
530 | |
531 | /** |
532 | * @param Participant $participant |
533 | * @param bool $canRemoveParticipants |
534 | * @param bool $canEmailParticipants |
535 | * @param UserIdentity $viewingUser |
536 | * @param array $nonPIIQuestionIDs |
537 | * @param bool $userCanViewNonPIIParticipantsData |
538 | * @return Tag |
539 | */ |
540 | private function getParticipantRow( |
541 | Participant $participant, |
542 | bool $canRemoveParticipants, |
543 | bool $canEmailParticipants, |
544 | UserIdentity $viewingUser, |
545 | array $nonPIIQuestionIDs, |
546 | bool $userCanViewNonPIIParticipantsData |
547 | ): Tag { |
548 | $row = new Tag( 'tr' ); |
549 | $performer = $this->userFactory->newFromId( $viewingUser->getId() ); |
550 | try { |
551 | $userName = $this->centralUserLookup->getUserName( $participant->getUser() ); |
552 | $genderUserName = $userName; |
553 | $user = $this->userFactory->newFromName( $userName ); |
554 | $userLinkComponents = $this->userLinker->getUserPagePath( $participant->getUser() ); |
555 | } catch ( CentralUserNotFoundException | HiddenCentralUserException $_ ) { |
556 | $user = null; |
557 | $userName = null; |
558 | $genderUserName = '@'; |
559 | } |
560 | $recipientIsValid = $user !== null && $canEmailParticipants && |
561 | $this->userMailer->validateTarget( $user, $performer ) === null; |
562 | $userLink = $this->userLinker->generateUserLinkWithFallback( |
563 | $participant->getUser(), |
564 | $this->language->getCode() |
565 | ); |
566 | |
567 | if ( $canRemoveParticipants ) { |
568 | $checkboxCell = new Tag( 'td' ); |
569 | $checkboxCell->addClasses( [ 'ext-campaignevents-eventdetails-user-row-checkbox' ] ); |
570 | $userId = $participant->getUser()->getCentralID(); |
571 | $checkbox = new CheckboxInputWidget( [ |
572 | 'name' => 'event-details-participants-checkboxes', |
573 | 'infusable' => true, |
574 | 'value' => $userId, |
575 | 'classes' => [ 'ext-campaignevents-event-details-participants-checkboxes' ], |
576 | 'data' => [ |
577 | 'canReceiveEmail' => $recipientIsValid, |
578 | 'username' => $userName, |
579 | 'userId' => $userId, |
580 | 'userPageLink' => $userLinkComponents ?? "" |
581 | ], |
582 | ] ); |
583 | $checkboxField = new FieldLayout( |
584 | $checkbox, |
585 | [ |
586 | 'label' => Sanitizer::stripAllTags( $userLink ), |
587 | 'invisibleLabel' => true, |
588 | ] |
589 | ); |
590 | $checkboxCell->appendContent( $checkboxField ); |
591 | $row->appendContent( $checkboxCell ); |
592 | } |
593 | |
594 | $usernameElement = new HtmlSnippet( $userLink ); |
595 | $usernameCell = ( new Tag( 'td' ) ) |
596 | ->appendContent( $usernameElement ); |
597 | |
598 | if ( $participant->isPrivateRegistration() ) { |
599 | $labelText = $this->msgFormatter->format( |
600 | MessageValue::new( 'campaignevents-event-details-private-participant-label', [ $genderUserName ] ) |
601 | ); |
602 | $privateIcon = new IconWidget( [ |
603 | 'icon' => 'lock', |
604 | 'label' => $labelText, |
605 | 'title' => $labelText, |
606 | 'classes' => [ 'ext-campaignevents-eventdetails-participants-private-icon' ] |
607 | ] ); |
608 | $usernameCell->appendContent( $privateIcon ); |
609 | } |
610 | $row->appendContent( $usernameCell ); |
611 | |
612 | $registrationDateCell = new Tag( 'td' ); |
613 | $registrationDateCell->appendContent( |
614 | $this->language->userTimeAndDate( |
615 | $participant->getRegisteredAt(), |
616 | $viewingUser |
617 | ) |
618 | ); |
619 | $row->appendContent( $registrationDateCell ); |
620 | |
621 | if ( $canEmailParticipants ) { |
622 | $row->appendContent( ( new Tag( 'td' ) )->appendContent( |
623 | $recipientIsValid |
624 | ? $this->msgFormatter->format( MessageValue::new( 'campaignevents-email-participants-yes' ) ) |
625 | : $this->msgFormatter->format( MessageValue::new( 'campaignevents-email-participants-no' ) ) |
626 | ) ); |
627 | } |
628 | |
629 | if ( !$this->isPastEvent && $userCanViewNonPIIParticipantsData ) { |
630 | $row = $this->addNonPIIParticipantAnswers( $row, $participant, $nonPIIQuestionIDs, $genderUserName ); |
631 | } |
632 | return $row |
633 | ->addClasses( [ 'ext-campaignevents-details-user-row' ] ); |
634 | } |
635 | |
636 | /** |
637 | * @param Tag $row |
638 | * @param Participant $participant |
639 | * @param array $nonPIIQuestionIDs |
640 | * @param string $genderUserName |
641 | * @return Tag |
642 | */ |
643 | private function addNonPIIParticipantAnswers( |
644 | Tag $row, |
645 | Participant $participant, |
646 | array $nonPIIQuestionIDs, |
647 | string $genderUserName |
648 | ): Tag { |
649 | if ( !$nonPIIQuestionIDs ) { |
650 | return $row; |
651 | } |
652 | |
653 | if ( $participant->getAggregationTimestamp() ) { |
654 | $aggregatedMessage = $this->msgFormatter->format( |
655 | MessageValue::new( 'campaignevents-participant-question-have-been-aggregated', [ $genderUserName ] ) |
656 | ); |
657 | $td = ( new Tag( 'td' ) )->setAttributes( [ 'colspan' => count( $nonPIIQuestionIDs ) ] ) |
658 | ->appendContent( $aggregatedMessage ) |
659 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-responses-aggregated-notice' ] ); |
660 | $row->appendContent( $td ); |
661 | return $row; |
662 | } else { |
663 | $answeredQuestions = []; |
664 | foreach ( $participant->getAnswers() as $answer ) { |
665 | $answeredQuestions[ $answer->getQuestionDBID() ] = $answer; |
666 | } |
667 | |
668 | $noResponseMessage = $this->msgFormatter->format( |
669 | MessageValue::new( 'campaignevents-participant-question-no-response' ) |
670 | ); |
671 | foreach ( $nonPIIQuestionIDs as $nonPIIQuestionID ) { |
672 | if ( array_key_exists( $nonPIIQuestionID, $answeredQuestions ) ) { |
673 | $nonPIIAnswer = $this->getQuestionAnswer( $answeredQuestions[ $nonPIIQuestionID ] ); |
674 | $row->appendContent( ( new Tag( 'td' ) )->appendContent( $nonPIIAnswer ) ); |
675 | } else { |
676 | $row->appendContent( ( new Tag( 'td' ) )->appendContent( $noResponseMessage ) ); |
677 | } |
678 | } |
679 | } |
680 | return $row; |
681 | } |
682 | |
683 | /** |
684 | * @param Answer $answer |
685 | * @return string |
686 | */ |
687 | private function getQuestionAnswer( Answer $answer ) { |
688 | $option = $answer->getOption(); |
689 | if ( $option === null ) { |
690 | return $this->msgFormatter->format( |
691 | MessageValue::new( 'campaignevents-participant-question-no-response' ) |
692 | ); |
693 | } |
694 | $optionMessageKey = $this->eventQuestionsRegistry->getQuestionOptionMessageByID( |
695 | $answer->getQuestionDBID(), |
696 | $option |
697 | ); |
698 | $participantAnswer = $this->msgFormatter->format( MessageValue::new( $optionMessageKey ) ); |
699 | if ( $answer->getText() ) { |
700 | $participantAnswer .= $this->msgFormatter->format( |
701 | MessageValue::new( 'colon-separator' ) |
702 | ) . $answer->getText(); |
703 | } |
704 | return $participantAnswer; |
705 | } |
706 | |
707 | /** |
708 | * @param int $eventID |
709 | * @param bool $userCanViewNonPIIParticipantsData |
710 | * @param ExistingEventRegistration $event |
711 | * @param OutputPage $out |
712 | * @return Tag|null |
713 | */ |
714 | private function getFooter( |
715 | int $eventID, |
716 | bool $userCanViewNonPIIParticipantsData, |
717 | ExistingEventRegistration $event, |
718 | OutputPage $out |
719 | ): ?Tag { |
720 | $privateParticipantsCount = $this->participantsStore->getPrivateParticipantCountForEvent( $eventID ); |
721 | |
722 | $footer = ( new Tag( 'div' ) )->addClasses( [ 'ext-campaignevents-eventdetails-participants-footer' ] ); |
723 | if ( $privateParticipantsCount > 0 ) { |
724 | $icon = new IconWidget( [ 'icon' => 'lock' ] ); |
725 | $text = $this->msgFormatter->format( |
726 | MessageValue::new( 'campaignevents-event-details-participants-private' ) |
727 | ->numParams( $privateParticipantsCount ) |
728 | ); |
729 | $textElement = ( new Tag( 'span' ) ) |
730 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-private-count-msg' ] ) |
731 | ->setAttributes( [ 'data-mw-count' => $privateParticipantsCount ] ) |
732 | ->appendContent( $text ); |
733 | $privateParticipants = ( new Tag( 'div' ) ) |
734 | ->addClasses( [ 'ext-campaignevents-eventdetails-participants-private-count-footer' ] ) |
735 | ->appendContent( $icon, $textElement ); |
736 | $footer->appendContent( $privateParticipants ); |
737 | } |
738 | |
739 | if ( |
740 | $event->getParticipantQuestions() && |
741 | $this->isPastEvent && |
742 | $userCanViewNonPIIParticipantsData |
743 | ) { |
744 | $deletedNonPiiInfoNoticeElement = new MessageWidget( [ |
745 | 'type' => 'notice', |
746 | 'label' => new HtmlSnippet( |
747 | $out->msg( 'campaignevents-event-details-participants-individual-data-deleted' ) |
748 | ->params( $this->statisticsTabUrl )->parse() |
749 | ), |
750 | 'inline' => true |
751 | ] ); |
752 | $deletedNonPiiInfoNoticeElement->addClasses( |
753 | [ 'ext-campaignevents-eventdetails-participants-individual-data-deleted-notice' ] |
754 | ); |
755 | |
756 | $footer->appendContent( $deletedNonPiiInfoNoticeElement ); |
757 | } |
758 | return $footer; |
759 | } |
760 | |
761 | /** |
762 | * @param bool $viewerCanRemoveParticipants |
763 | * @param bool $viewerCanEmailParticipants |
764 | * @return Tag |
765 | */ |
766 | private function getHeaderControls( |
767 | bool $viewerCanRemoveParticipants, |
768 | bool $viewerCanEmailParticipants |
769 | ): Tag { |
770 | $container = ( new Tag( 'div' ) )->addClasses( [ 'ext-campaignevents-eventdetails-participants-controls' ] ); |
771 | $deselectButton = new ButtonWidget( [ |
772 | 'icon' => 'close', |
773 | 'title' => $this->msgFormatter->format( |
774 | MessageValue::new( 'campaignevents-event-details-participants-deselect' ) |
775 | ), |
776 | 'framed' => false, |
777 | 'flags' => [ 'progressive' ], |
778 | 'infusable' => true, |
779 | 'label' => $this->msgFormatter->format( |
780 | MessageValue::new( 'campaignevents-event-details-participants-checkboxes-selected', [ 0, 0 ] ) |
781 | ), |
782 | 'classes' => [ 'ext-campaignevents-eventdetails-participants-count-button' ] |
783 | ] ); |
784 | $container->appendContent( [ $deselectButton ] ); |
785 | |
786 | $extraButtons = []; |
787 | if ( $viewerCanRemoveParticipants ) { |
788 | $extraButtons[] = new ButtonWidget( [ |
789 | 'infusable' => true, |
790 | 'framed' => true, |
791 | 'flags' => [ |
792 | 'destructive' |
793 | ], |
794 | 'label' => $this->msgFormatter->format( |
795 | MessageValue::new( 'campaignevents-event-details-remove-participant-remove-btn' ) |
796 | ), |
797 | 'id' => 'ext-campaignevents-event-details-remove-participant-button', |
798 | 'classes' => [ |
799 | 'ext-campaignevents-event-details-remove-participant-button', |
800 | 'ext-campaignevents-eventdetails-hide-element' |
801 | ], |
802 | ] ); |
803 | } |
804 | if ( $viewerCanEmailParticipants ) { |
805 | $extraButtons[] = new ButtonWidget( [ |
806 | 'infusable' => true, |
807 | 'framed' => true, |
808 | 'label' => $this->msgFormatter->format( |
809 | MessageValue::new( 'campaignevents-event-details-message-all' ) |
810 | ), |
811 | 'flags' => [ 'progressive' ], |
812 | 'classes' => [ 'ext-campaignevents-eventdetails-message-all-participants-button' ], |
813 | ] ); |
814 | } |
815 | |
816 | if ( $extraButtons ) { |
817 | $container->appendContent( new ButtonGroupWidget( [ |
818 | 'items' => $extraButtons, |
819 | 'classes' => [ 'ext-campaignevents-eventdetails-extra-buttons' ], |
820 | ] ) ); |
821 | } |
822 | return $container; |
823 | } |
824 | } |