Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 698 |
|
0.00% |
0 / 23 |
CRAP | |
0.00% |
0 / 1 |
EventPageDecorator | |
0.00% |
0 / 698 |
|
0.00% |
0 / 23 |
9312 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
2 | |||
decoratePage | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
maybeAddEnableRegistrationHeader | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
getEnableRegistrationHeader | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
2 | |||
addRegistrationHeader | |
0.00% |
0 / 64 |
|
0.00% |
0 / 1 |
20 | |||
getEventQuestionsData | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
56 | |||
getHeaderElement | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
getParticipantNoticeRow | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getEventInfoHeaderRow | |
0.00% |
0 / 89 |
|
0.00% |
0 / 1 |
30 | |||
getDetailsDialogContent | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
2 | |||
getDetailsDialogOrganizers | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
12 | |||
getDetailsDialogEventInfo | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
getDetailsDialogDates | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
2 | |||
getDetailsDialogLocation | |
0.00% |
0 / 66 |
|
0.00% |
0 / 1 |
182 | |||
getDetailsDialogChat | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
90 | |||
getDetailsDialogParticipants | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
20 | |||
getParticipantRows | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
56 | |||
getActionElement | |
0.00% |
0 / 66 |
|
0.00% |
0 / 1 |
72 | |||
getUserStatus | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
182 | |||
getParticipantFooter | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getParticipantRow | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
12 | |||
makeDetailsDialogSection | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
getDetailsDialogWikis | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents\EventPage; |
6 | |
7 | use LogicException; |
8 | use MediaWiki\Extension\CampaignEvents\Event\EventRegistration; |
9 | use MediaWiki\Extension\CampaignEvents\Event\ExistingEventRegistration; |
10 | use MediaWiki\Extension\CampaignEvents\Event\PageEventLookup; |
11 | use MediaWiki\Extension\CampaignEvents\Formatters\EventFormatter; |
12 | use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsCentralUserLookup; |
13 | use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsPageFactory; |
14 | use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUser; |
15 | use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUserNotFoundException; |
16 | use MediaWiki\Extension\CampaignEvents\MWEntity\HiddenCentralUserException; |
17 | use MediaWiki\Extension\CampaignEvents\MWEntity\ICampaignsAuthority; |
18 | use MediaWiki\Extension\CampaignEvents\MWEntity\ICampaignsPage; |
19 | use MediaWiki\Extension\CampaignEvents\MWEntity\MWAuthorityProxy; |
20 | use MediaWiki\Extension\CampaignEvents\MWEntity\UserLinker; |
21 | use MediaWiki\Extension\CampaignEvents\MWEntity\UserNotGlobalException; |
22 | use MediaWiki\Extension\CampaignEvents\MWEntity\WikiLookup; |
23 | use MediaWiki\Extension\CampaignEvents\Organizers\OrganizersStore; |
24 | use MediaWiki\Extension\CampaignEvents\Participants\Participant; |
25 | use MediaWiki\Extension\CampaignEvents\Participants\ParticipantsStore; |
26 | use MediaWiki\Extension\CampaignEvents\Participants\RegisterParticipantCommand; |
27 | use MediaWiki\Extension\CampaignEvents\Participants\UnregisterParticipantCommand; |
28 | use MediaWiki\Extension\CampaignEvents\Permissions\PermissionChecker; |
29 | use MediaWiki\Extension\CampaignEvents\Questions\EventQuestionsRegistry; |
30 | use MediaWiki\Extension\CampaignEvents\Special\AbstractEventRegistrationSpecialPage; |
31 | use MediaWiki\Extension\CampaignEvents\Special\SpecialCancelEventRegistration; |
32 | use MediaWiki\Extension\CampaignEvents\Special\SpecialEnableEventRegistration; |
33 | use MediaWiki\Extension\CampaignEvents\Special\SpecialEventDetails; |
34 | use MediaWiki\Extension\CampaignEvents\Special\SpecialRegisterForEvent; |
35 | use MediaWiki\Extension\CampaignEvents\Time\EventTimeFormatter; |
36 | use MediaWiki\Extension\CampaignEvents\Utils; |
37 | use MediaWiki\Extension\CampaignEvents\Widget\TextWithIconWidget; |
38 | use MediaWiki\Html\Html; |
39 | use MediaWiki\Language\Language; |
40 | use MediaWiki\Linker\LinkRenderer; |
41 | use MediaWiki\Output\OutputPage; |
42 | use MediaWiki\Page\ProperPageIdentity; |
43 | use MediaWiki\Permissions\Authority; |
44 | use MediaWiki\SpecialPage\SpecialPage; |
45 | use MediaWiki\User\UserIdentity; |
46 | use OOUI\ButtonWidget; |
47 | use OOUI\Element; |
48 | use OOUI\HorizontalLayout; |
49 | use OOUI\HtmlSnippet; |
50 | use OOUI\IconWidget; |
51 | use OOUI\MessageWidget; |
52 | use OOUI\PanelLayout; |
53 | use OOUI\Tag; |
54 | use UnexpectedValueException; |
55 | use Wikimedia\Message\IMessageFormatterFactory; |
56 | use Wikimedia\Message\ITextFormatter; |
57 | use Wikimedia\Message\MessageValue; |
58 | |
59 | /** |
60 | * This service is used to add some widgets to the event page, like the registration header. |
61 | */ |
62 | class EventPageDecorator { |
63 | public const SERVICE_NAME = 'CampaignEventsEventPageDecorator'; |
64 | |
65 | private const ADDRESS_MAX_LENGTH = 30; |
66 | // See T304719#7909758 for how these numbers were chosen |
67 | private const ORGANIZERS_LIMIT = 4; |
68 | private const PARTICIPANTS_LIMIT = 10; |
69 | |
70 | // Constants for the different statuses of a user wrt a given event registration |
71 | private const USER_STATUS_BLOCKED = 1; |
72 | private const USER_STATUS_ORGANIZER = 2; |
73 | private const USER_STATUS_PARTICIPANT_CAN_UNREGISTER = 3; |
74 | private const USER_STATUS_CAN_REGISTER = 4; |
75 | private const USER_STATUS_CANNOT_REGISTER_ENDED = 5; |
76 | private const USER_STATUS_CANNOT_REGISTER_CLOSED = 6; |
77 | |
78 | private PageEventLookup $pageEventLookup; |
79 | private ParticipantsStore $participantsStore; |
80 | private OrganizersStore $organizersStore; |
81 | private PermissionChecker $permissionChecker; |
82 | private LinkRenderer $linkRenderer; |
83 | private CampaignsPageFactory $campaignsPageFactory; |
84 | private CampaignsCentralUserLookup $centralUserLookup; |
85 | private UserLinker $userLinker; |
86 | private EventTimeFormatter $eventTimeFormatter; |
87 | private EventPageCacheUpdater $eventPageCacheUpdater; |
88 | private EventQuestionsRegistry $eventQuestionsRegistry; |
89 | private WikiLookup $wikiLookup; |
90 | |
91 | private Language $language; |
92 | private ICampaignsAuthority $authority; |
93 | private UserIdentity $viewingUser; |
94 | private OutputPage $out; |
95 | private ITextFormatter $msgFormatter; |
96 | |
97 | /** |
98 | * @var bool|null Whether the user is registered publicly or privately. This value is lazy-loaded iff the user |
99 | * status is USER_STATUS_PARTICIPANT_CAN_UNREGISTER. |
100 | */ |
101 | private ?bool $participantIsPublic = null; |
102 | |
103 | /** |
104 | * @param PageEventLookup $pageEventLookup |
105 | * @param ParticipantsStore $participantsStore |
106 | * @param OrganizersStore $organizersStore |
107 | * @param PermissionChecker $permissionChecker |
108 | * @param IMessageFormatterFactory $messageFormatterFactory |
109 | * @param LinkRenderer $linkRenderer |
110 | * @param CampaignsPageFactory $campaignsPageFactory |
111 | * @param CampaignsCentralUserLookup $centralUserLookup |
112 | * @param UserLinker $userLinker |
113 | * @param EventTimeFormatter $eventTimeFormatter |
114 | * @param EventPageCacheUpdater $eventPageCacheUpdater |
115 | * @param EventQuestionsRegistry $eventQuestionsRegistry |
116 | * @param WikiLookup $wikiLookup |
117 | * @param Language $language |
118 | * @param Authority $viewingAuthority |
119 | * @param OutputPage $out |
120 | */ |
121 | public function __construct( |
122 | PageEventLookup $pageEventLookup, |
123 | ParticipantsStore $participantsStore, |
124 | OrganizersStore $organizersStore, |
125 | PermissionChecker $permissionChecker, |
126 | IMessageFormatterFactory $messageFormatterFactory, |
127 | LinkRenderer $linkRenderer, |
128 | CampaignsPageFactory $campaignsPageFactory, |
129 | CampaignsCentralUserLookup $centralUserLookup, |
130 | UserLinker $userLinker, |
131 | EventTimeFormatter $eventTimeFormatter, |
132 | EventPageCacheUpdater $eventPageCacheUpdater, |
133 | EventQuestionsRegistry $eventQuestionsRegistry, |
134 | WikiLookup $wikiLookup, |
135 | Language $language, |
136 | Authority $viewingAuthority, |
137 | OutputPage $out |
138 | ) { |
139 | $this->pageEventLookup = $pageEventLookup; |
140 | $this->participantsStore = $participantsStore; |
141 | $this->organizersStore = $organizersStore; |
142 | $this->permissionChecker = $permissionChecker; |
143 | $this->linkRenderer = $linkRenderer; |
144 | $this->campaignsPageFactory = $campaignsPageFactory; |
145 | $this->centralUserLookup = $centralUserLookup; |
146 | $this->userLinker = $userLinker; |
147 | $this->eventTimeFormatter = $eventTimeFormatter; |
148 | $this->eventPageCacheUpdater = $eventPageCacheUpdater; |
149 | $this->eventQuestionsRegistry = $eventQuestionsRegistry; |
150 | $this->wikiLookup = $wikiLookup; |
151 | |
152 | $this->language = $language; |
153 | $this->authority = new MWAuthorityProxy( $viewingAuthority ); |
154 | $this->viewingUser = $viewingAuthority->getUser(); |
155 | $this->out = $out; |
156 | $this->msgFormatter = $messageFormatterFactory->getTextFormatter( $language->getCode() ); |
157 | } |
158 | |
159 | /** |
160 | * This is the main entry point for this class. It adds all the necessary HTML (registration header, popup etc.) |
161 | * to the given OutputPage, as well as loading some JS/CSS resources. |
162 | * |
163 | * @param ProperPageIdentity $page |
164 | */ |
165 | public function decoratePage( ProperPageIdentity $page ): void { |
166 | $registration = $this->pageEventLookup->getRegistrationForLocalPage( $page ); |
167 | |
168 | if ( $registration && $registration->getDeletionTimestamp() !== null ) { |
169 | return; |
170 | } |
171 | |
172 | if ( $registration ) { |
173 | $this->addRegistrationHeader( $page, $registration ); |
174 | $this->eventPageCacheUpdater->adjustCacheForPageWithRegistration( $this->out, $registration ); |
175 | } else { |
176 | $campaignsPage = $this->campaignsPageFactory->newFromLocalMediaWikiPage( $page ); |
177 | $this->maybeAddEnableRegistrationHeader( $campaignsPage ); |
178 | } |
179 | } |
180 | |
181 | /** |
182 | * @param ICampaignsPage $eventPage |
183 | */ |
184 | private function maybeAddEnableRegistrationHeader( ICampaignsPage $eventPage ): void { |
185 | if ( !$this->permissionChecker->userCanEnableRegistration( $this->authority, $eventPage ) ) { |
186 | return; |
187 | } |
188 | |
189 | $this->out->enableOOUI(); |
190 | $this->out->addModuleStyles( [ |
191 | 'ext.campaignEvents.eventpage.styles', |
192 | 'oojs-ui.styles.icons-editing-advanced', |
193 | ] ); |
194 | $this->out->addModules( [ 'ext.campaignEvents.eventpage' ] ); |
195 | // We pass this to the client to avoid hardcoding the name of the page field in JS. Apparently we can't use |
196 | // a RL callback for this because it doesn't provide the current page. |
197 | $enableRegistrationURL = SpecialPage::getTitleFor( SpecialEnableEventRegistration::PAGE_NAME )->getLocalURL( [ |
198 | SpecialEnableEventRegistration::PAGE_FIELD_NAME => $eventPage->getPrefixedText() |
199 | ] ); |
200 | $this->out->addJsConfigVars( [ 'wgCampaignEventsEnableRegistrationURL' => $enableRegistrationURL ] ); |
201 | $this->out->addHTML( $this->getEnableRegistrationHeader( $enableRegistrationURL ) ); |
202 | } |
203 | |
204 | /** |
205 | * @param string $enableRegistrationURL |
206 | * @return Tag |
207 | */ |
208 | private function getEnableRegistrationHeader( string $enableRegistrationURL ): Tag { |
209 | $organizerText = ( new Tag( 'div' ) )->appendContent( |
210 | $this->msgFormatter->format( MessageValue::new( 'campaignevents-eventpage-enableheader-organizer' ) ) |
211 | )->setAttributes( [ 'class' => 'ext-campaignevents-eventpage-organizer-label' ] ); |
212 | |
213 | // Wrap it in a span for use inside a flex container, since the message contains HTML. |
214 | // XXX Can't use ITextFormatter here because the message contains HTML, see T260689 |
215 | $infoMsg = ( new Tag( 'span' ) )->appendContent( |
216 | new HtmlSnippet( $this->out->msg( 'campaignevents-eventpage-enableheader-eventpage-desc' )->parse() ) |
217 | ); |
218 | $infoText = ( new Tag( 'div' ) )->appendContent( |
219 | new IconWidget( [ 'icon' => 'calendar', 'classes' => [ 'ext-campaignevents-eventpage-icon' ] ] ), |
220 | $infoMsg |
221 | )->setAttributes( [ 'class' => 'ext-campaignevents-eventpage-enableheader-message' ] ); |
222 | $infoElement = ( new Tag( 'div' ) )->appendContent( $organizerText, $infoText ); |
223 | |
224 | $enableRegistrationBtn = new ButtonWidget( [ |
225 | 'flags' => [ 'primary', 'progressive' ], |
226 | 'label' => $this->msgFormatter->format( |
227 | MessageValue::new( 'campaignevents-eventpage-enableheader-button-label' ) |
228 | ), |
229 | 'classes' => [ 'ext-campaignevents-eventpage-enable-registration-btn' ], |
230 | 'href' => $enableRegistrationURL, |
231 | ] ); |
232 | |
233 | $layout = new PanelLayout( [ |
234 | 'content' => [ $infoElement, $enableRegistrationBtn ], |
235 | 'padded' => true, |
236 | 'framed' => true, |
237 | 'expanded' => false, |
238 | 'classes' => [ 'ext-campaignevents-eventpage-enableheader' ], |
239 | ] ); |
240 | |
241 | $layout->setAttributes( [ |
242 | // Set the lang/dir explicitly, otherwise it will use that of the site/page language, |
243 | // not that of the interface. |
244 | 'dir' => $this->language->getDir(), |
245 | 'lang' => $this->language->getHtmlCode() |
246 | ] ); |
247 | return $layout; |
248 | } |
249 | |
250 | private function addRegistrationHeader( ProperPageIdentity $page, ExistingEventRegistration $registration ): void { |
251 | $this->out->setPreventClickjacking( true ); |
252 | $this->out->enableOOUI(); |
253 | $this->out->addModuleStyles( array_merge( |
254 | [ |
255 | 'ext.campaignEvents.eventpage.styles', |
256 | 'oojs-ui.styles.icons-location', |
257 | 'oojs-ui.styles.icons-interactions', |
258 | 'oojs-ui.styles.icons-moderation', |
259 | 'oojs-ui.styles.icons-user', |
260 | 'oojs-ui.styles.icons-alerts', |
261 | 'oojs-ui.styles.icons-wikimedia' |
262 | ], |
263 | UserLinker::MODULE_STYLES |
264 | ) ); |
265 | |
266 | $this->out->addModules( [ 'ext.campaignEvents.eventpage' ] ); |
267 | |
268 | try { |
269 | $centralUser = $this->centralUserLookup->newFromAuthority( $this->authority ); |
270 | $curParticipant = $this->participantsStore->getEventParticipant( |
271 | $registration->getID(), |
272 | $centralUser, |
273 | true |
274 | ); |
275 | $hasAggregatedAnswers = $this->participantsStore->userHasAggregatedAnswers( |
276 | $registration->getID(), |
277 | $centralUser |
278 | ); |
279 | } catch ( UserNotGlobalException $_ ) { |
280 | $centralUser = null; |
281 | $curParticipant = null; |
282 | $hasAggregatedAnswers = false; |
283 | } |
284 | |
285 | $userStatus = $this->getUserStatus( $registration, $centralUser, $curParticipant ); |
286 | |
287 | $this->out->addHTML( $this->getHeaderElement( $registration, $userStatus ) ); |
288 | $this->out->addHTML( |
289 | $this->getDetailsDialogContent( |
290 | $page, |
291 | $registration, |
292 | $userStatus, |
293 | $curParticipant |
294 | ) |
295 | ); |
296 | |
297 | $aggregationTimestamp = $curParticipant |
298 | ? Utils::getAnswerAggregationTimestamp( $curParticipant, $registration ) |
299 | : null; |
300 | |
301 | $session = $this->out->getRequest()->getSession(); |
302 | $registrationUpdatedVal = $session |
303 | ->get( AbstractEventRegistrationSpecialPage::REGISTRATION_UPDATED_SESSION_KEY ); |
304 | $registrationUpdatedWarnings = []; |
305 | $isNewRegistration = false; |
306 | if ( $registrationUpdatedVal ) { |
307 | // User just updated registration, show a success notification, plus any warnings. |
308 | $registrationUpdatedWarnings = $session |
309 | ->get( AbstractEventRegistrationSpecialPage::REGISTRATION_UPDATED_WARNINGS_SESSION_KEY, [] ); |
310 | $isNewRegistration = $registrationUpdatedVal === |
311 | AbstractEventRegistrationSpecialPage::REGISTRATION_UPDATED_SESSION_ENABLED; |
312 | $session->remove( AbstractEventRegistrationSpecialPage::REGISTRATION_UPDATED_SESSION_KEY ); |
313 | $session->remove( AbstractEventRegistrationSpecialPage::REGISTRATION_UPDATED_WARNINGS_SESSION_KEY ); |
314 | } |
315 | |
316 | $this->out->addJsConfigVars( [ |
317 | 'wgCampaignEventsEventID' => $registration->getID(), |
318 | 'wgCampaignEventsParticipantIsPublic' => $this->participantIsPublic, |
319 | 'wgCampaignEventsEventQuestions' => $this->getEventQuestionsData( $registration, $curParticipant ), |
320 | 'wgCampaignEventsAnswersAlreadyAggregated' => $hasAggregatedAnswers, |
321 | 'wgCampaignEventsAggregationTimestamp' => $aggregationTimestamp, |
322 | 'wgCampaignEventsRegistrationUpdated' => (bool)$registrationUpdatedVal, |
323 | 'wgCampaignEventsIsNewRegistration' => $isNewRegistration, |
324 | 'wgCampaignEventsRegistrationUpdatedWarnings' => $registrationUpdatedWarnings, |
325 | ] ); |
326 | } |
327 | |
328 | /** |
329 | * @param ExistingEventRegistration $registration |
330 | * @param Participant|null $participant |
331 | * @return array[] |
332 | */ |
333 | private function getEventQuestionsData( |
334 | ExistingEventRegistration $registration, |
335 | ?Participant $participant |
336 | ): array { |
337 | $enabledQuestions = $registration->getParticipantQuestions(); |
338 | $curAnswers = $participant ? $participant->getAnswers() : []; |
339 | $questionsToShow = EventQuestionsRegistry::getParticipantQuestionsToShow( $enabledQuestions, $curAnswers ); |
340 | |
341 | $questionsData = []; |
342 | $questionsAPI = $this->eventQuestionsRegistry->getQuestionsForAPI( $questionsToShow ); |
343 | // Localise all messages to avoid having to do that in the client side. |
344 | foreach ( $questionsAPI as $questionAPIData ) { |
345 | $curQuestionData = [ |
346 | 'type' => $questionAPIData['type'], |
347 | 'label' => $this->msgFormatter->format( MessageValue::new( $questionAPIData['label-message'] ) ), |
348 | ]; |
349 | if ( isset( $questionAPIData['options-messages'] ) ) { |
350 | $curQuestionData['options'] = []; |
351 | foreach ( $questionAPIData['options-messages'] as $messageKey => $value ) { |
352 | $message = $this->msgFormatter->format( MessageValue::new( $messageKey ) ); |
353 | $curQuestionData['options'][$messageKey] = [ |
354 | 'value' => $value, |
355 | 'message' => $message |
356 | ]; |
357 | } |
358 | } |
359 | if ( isset( $questionAPIData['other-options'] ) ) { |
360 | $curQuestionData['other-options'] = []; |
361 | foreach ( $questionAPIData['other-options'] as $showIfVal => $otherOptData ) { |
362 | $curQuestionData['other-options'][$showIfVal] = [ |
363 | 'type' => $otherOptData['type'], |
364 | 'placeholder' => $this->msgFormatter->format( |
365 | MessageValue::new( $otherOptData['label-message'] ) |
366 | ), |
367 | ]; |
368 | } |
369 | } |
370 | $questionsData[$questionAPIData['name']] = $curQuestionData; |
371 | } |
372 | |
373 | return [ |
374 | 'questions' => $questionsData, |
375 | 'answers' => $this->eventQuestionsRegistry->formatAnswersForAPI( $curAnswers, $enabledQuestions ) |
376 | ]; |
377 | } |
378 | |
379 | /** |
380 | * Returns the header element. |
381 | * |
382 | * @param ExistingEventRegistration $registration |
383 | * @param int $userStatus One of the self::USER_STATUS_* constants |
384 | * @return Tag |
385 | */ |
386 | private function getHeaderElement( |
387 | ExistingEventRegistration $registration, |
388 | int $userStatus |
389 | ): Tag { |
390 | $content = []; |
391 | |
392 | $participantNoticeRow = $this->getParticipantNoticeRow( $userStatus ); |
393 | if ( $participantNoticeRow ) { |
394 | $content[] = $participantNoticeRow; |
395 | } |
396 | |
397 | $content[] = $this->getEventInfoHeaderRow( $registration, $userStatus ); |
398 | |
399 | $layout = new PanelLayout( [ |
400 | 'content' => $content, |
401 | 'padded' => true, |
402 | 'framed' => true, |
403 | 'expanded' => false, |
404 | 'classes' => [ 'ext-campaignevents-eventpage-header' ], |
405 | ] ); |
406 | |
407 | $layout->setAttributes( [ |
408 | // Set the lang/dir explicitly, otherwise it will use that of the site/page language, |
409 | // not that of the interface. |
410 | 'dir' => $this->language->getDir(), |
411 | 'lang' => $this->language->getHtmlCode() |
412 | ] ); |
413 | |
414 | return $layout; |
415 | } |
416 | |
417 | /** |
418 | * @param int $userStatus |
419 | * @return Tag|null |
420 | */ |
421 | private function getParticipantNoticeRow( int $userStatus ): ?Tag { |
422 | if ( $userStatus !== self::USER_STATUS_PARTICIPANT_CAN_UNREGISTER ) { |
423 | return null; |
424 | } |
425 | $msg = $this->participantIsPublic |
426 | ? 'campaignevents-eventpage-header-registered-publicly' |
427 | : 'campaignevents-eventpage-header-registered-privately'; |
428 | return new MessageWidget( [ |
429 | 'type' => 'success', |
430 | 'label' => $this->msgFormatter->format( MessageValue::new( $msg ) ), |
431 | 'inline' => true, |
432 | 'classes' => [ 'ext-campaignevents-eventpage-participant-notice' ] |
433 | ] ); |
434 | } |
435 | |
436 | /** |
437 | * @param ExistingEventRegistration $registration |
438 | * @param int $userStatus |
439 | * @return Tag |
440 | */ |
441 | private function getEventInfoHeaderRow( |
442 | ExistingEventRegistration $registration, |
443 | int $userStatus |
444 | ): Tag { |
445 | $eventID = $registration->getID(); |
446 | $items = []; |
447 | |
448 | $meetingType = $registration->getMeetingType(); |
449 | if ( $meetingType === EventRegistration::MEETING_TYPE_ONLINE_AND_IN_PERSON ) { |
450 | $locationContent = $this->msgFormatter->format( |
451 | MessageValue::new( 'campaignevents-eventpage-header-type-online-and-in-person' ) |
452 | ); |
453 | } elseif ( $meetingType & EventRegistration::MEETING_TYPE_ONLINE ) { |
454 | $locationContent = $this->msgFormatter->format( |
455 | MessageValue::new( 'campaignevents-eventpage-header-type-online' ) |
456 | ); |
457 | } else { |
458 | // In-person event |
459 | $address = $registration->getMeetingAddress(); |
460 | if ( $address !== null ) { |
461 | $locationContent = new Tag( 'div' ); |
462 | $locationContent->setAttributes( [ |
463 | 'dir' => Utils::guessStringDirection( $address ) |
464 | ] ); |
465 | $locationContent->addClasses( [ 'ext-campaignevents-eventpage-header-address' ] ); |
466 | $locationContent->appendContent( |
467 | $this->language->truncateForVisual( $address, self::ADDRESS_MAX_LENGTH ) |
468 | ); |
469 | } else { |
470 | $locationContent = $this->msgFormatter->format( |
471 | MessageValue::new( 'campaignevents-eventpage-header-type-in-person' ) |
472 | ); |
473 | } |
474 | } |
475 | $items[] = new TextWithIconWidget( [ |
476 | 'icon' => 'mapPin', |
477 | 'content' => $locationContent, |
478 | 'label' => $this->msgFormatter->format( |
479 | MessageValue::new( 'campaignevents-eventpage-header-location-label' ) |
480 | ), |
481 | 'icon_classes' => [ 'ext-campaignevents-eventpage-icon' ], |
482 | ] ); |
483 | |
484 | $formattedStart = $this->eventTimeFormatter->formatStart( $registration, $this->language, $this->viewingUser ); |
485 | $formattedEnd = $this->eventTimeFormatter->formatEnd( $registration, $this->language, $this->viewingUser ); |
486 | $datesMsg = $this->msgFormatter->format( |
487 | MessageValue::new( 'campaignevents-eventpage-header-dates' )->params( |
488 | $formattedStart->getTimeAndDate(), |
489 | $formattedStart->getDate(), |
490 | $formattedStart->getTime(), |
491 | $formattedEnd->getTimeAndDate(), |
492 | $formattedEnd->getDate(), |
493 | $formattedEnd->getTime() |
494 | ) |
495 | ); |
496 | $formattedTimezone = EventTimeFormatter::wrapTimeZoneForConversion( |
497 | $this->eventTimeFormatter->formatTimezone( $registration, $this->viewingUser ) |
498 | ); |
499 | // XXX Can't use ITextFormatter due to parse() |
500 | $timezoneMsg = $this->out->msg( 'campaignevents-eventpage-header-timezone' ) |
501 | ->params( $formattedTimezone ) |
502 | ->parse(); |
503 | $items[] = new TextWithIconWidget( [ |
504 | 'icon' => 'clock', |
505 | 'content' => [ |
506 | EventTimeFormatter::wrapRangeForConversion( $registration, $datesMsg ), |
507 | ( new Tag( 'div' ) )->appendContent( new HtmlSnippet( $timezoneMsg ) ) |
508 | ], |
509 | 'label' => $this->msgFormatter->format( |
510 | MessageValue::new( 'campaignevents-eventpage-header-dates-label' ) |
511 | ), |
512 | 'icon_classes' => [ 'ext-campaignevents-eventpage-icon' ], |
513 | 'classes' => [ 'ext-campaignevents-eventpage-header-time' ], |
514 | ] ); |
515 | |
516 | $items[] = new TextWithIconWidget( [ |
517 | 'icon' => 'userGroup', |
518 | 'content' => $this->msgFormatter->format( |
519 | MessageValue::new( 'campaignevents-eventpage-header-participants' ) |
520 | ->numParams( $this->participantsStore->getFullParticipantCountForEvent( $eventID ) ) |
521 | ), |
522 | 'label' => $this->msgFormatter->format( |
523 | MessageValue::new( 'campaignevents-eventpage-header-participants-label' ) |
524 | ), |
525 | 'icon_classes' => [ 'ext-campaignevents-eventpage-icon' ], |
526 | ] ); |
527 | |
528 | $btnContainer = ( new Tag( 'div' ) ) |
529 | ->addClasses( [ 'ext-campaignevents-eventpage-header-buttons' ] ); |
530 | $btnContainer->appendContent( new ButtonWidget( [ |
531 | 'framed' => false, |
532 | 'flags' => [ 'progressive' ], |
533 | 'label' => $this->msgFormatter->format( MessageValue::new( 'campaignevents-eventpage-header-details' ) ), |
534 | 'classes' => [ 'ext-campaignevents-eventpage-details-btn' ], |
535 | 'href' => SpecialPage::getTitleFor( SpecialEventDetails::PAGE_NAME, (string)$eventID )->getLocalURL(), |
536 | ] ) ); |
537 | |
538 | $actionElement = $this->getActionElement( $eventID, $userStatus ); |
539 | if ( $actionElement ) { |
540 | $btnContainer->appendContent( $actionElement ); |
541 | } |
542 | |
543 | $items[] = $btnContainer; |
544 | |
545 | return ( new Tag( 'div' ) ) |
546 | ->addClasses( [ 'ext-campaignevents-eventpage-header-eventinfo' ] ) |
547 | ->appendContent( ...$items ); |
548 | } |
549 | |
550 | /** |
551 | * Returns the content of the "more details" dialog. Unfortunately, we have to build it here rather then on the |
552 | * client side, for the following reasons: |
553 | * - There's no way to format dates according to the user preferences (T21992) |
554 | * - There's no easy way to get the directionality of a language (T181684) |
555 | * - Other utilities are missing (e.g., generating user links) |
556 | * - Secondarily, no need to make 3 API requests and worry about them failing. |
557 | * |
558 | * @param ProperPageIdentity $page |
559 | * @param ExistingEventRegistration $registration |
560 | * @param int $userStatus One of the self::USER_STATUS_* constants |
561 | * @param Participant|null $participant |
562 | * @return string |
563 | */ |
564 | private function getDetailsDialogContent( |
565 | ProperPageIdentity $page, |
566 | ExistingEventRegistration $registration, |
567 | int $userStatus, |
568 | ?Participant $participant |
569 | ): string { |
570 | $eventID = $registration->getID(); |
571 | $organizersCount = $this->organizersStore->getOrganizerCountForEvent( $eventID ); |
572 | |
573 | $eventInfoContainer = $this->getDetailsDialogEventInfo( |
574 | $page, |
575 | $registration, |
576 | $organizersCount, |
577 | $userStatus |
578 | ); |
579 | $participantsContainer = $this->getDetailsDialogParticipants( |
580 | $eventID, |
581 | $participant, |
582 | $registration |
583 | ); |
584 | |
585 | $dialogContent = Html::element( |
586 | 'h2', |
587 | [ 'class' => 'ext-campaignevents-detailsdialog-header' ], |
588 | $registration->getName() |
589 | ); |
590 | $dialogContent .= $this->getDetailsDialogOrganizers( |
591 | $eventID, |
592 | $organizersCount |
593 | ); |
594 | $dialogContent .= Html::rawElement( |
595 | 'div', |
596 | [ 'class' => 'ext-campaignevents-detailsdialog-body-container' ], |
597 | $eventInfoContainer . $participantsContainer |
598 | ); |
599 | |
600 | return Html::rawElement( |
601 | 'div', |
602 | [ 'id' => 'ext-campaignevents-eventpage-details-dialog-content' ], |
603 | $dialogContent |
604 | ); |
605 | } |
606 | |
607 | /** |
608 | * @param int $eventID |
609 | * @param int $organizersCount |
610 | * @return string |
611 | */ |
612 | private function getDetailsDialogOrganizers( |
613 | int $eventID, |
614 | int $organizersCount |
615 | ): string { |
616 | $partialOrganizers = $this->organizersStore->getEventOrganizers( $eventID, self::ORGANIZERS_LIMIT ); |
617 | |
618 | $organizerElements = []; |
619 | foreach ( $partialOrganizers as $organizer ) { |
620 | $organizerElements[] = $this->userLinker->generateUserLinkWithFallback( |
621 | $organizer->getUser(), |
622 | $this->language->getCode() |
623 | ); |
624 | } |
625 | // XXX We need to use OutputPage here because there's no supported way to change the format of |
626 | // MessageFormatterFactory... |
627 | $organizersStr = $this->out->msg( 'campaignevents-eventpage-dialog-organizers' ) |
628 | ->rawParams( $this->language->commaList( $organizerElements ) ) |
629 | ->numParams( count( $organizerElements ) ) |
630 | ->escaped(); |
631 | if ( count( $partialOrganizers ) < $organizersCount ) { |
632 | $organizersStr .= Html::rawElement( |
633 | 'p', |
634 | [], |
635 | $this->linkRenderer->makeKnownLink( |
636 | SpecialPage::getTitleFor( SpecialEventDetails::PAGE_NAME, (string)$eventID ), |
637 | $this->msgFormatter->format( |
638 | MessageValue::new( 'campaignevents-eventpage-dialog-organizers-view-all' ) |
639 | ) |
640 | ) |
641 | ); |
642 | } |
643 | |
644 | return Html::rawElement( |
645 | 'div', |
646 | [ 'class' => 'ext-campaignevents-detailsdialog-organizers' ], |
647 | $organizersStr |
648 | ); |
649 | } |
650 | |
651 | private function getDetailsDialogEventInfo( |
652 | ProperPageIdentity $page, |
653 | ExistingEventRegistration $registration, |
654 | int $organizersCount, |
655 | int $userStatus |
656 | ): string { |
657 | $eventInfo = $this->getDetailsDialogDates( $registration ); |
658 | $eventInfo .= $this->getDetailsDialogLocation( |
659 | $page, |
660 | $registration, |
661 | $organizersCount, |
662 | $userStatus |
663 | ); |
664 | if ( $registration->getWikis() ) { |
665 | $eventInfo .= $this->getDetailsDialogWikis( $registration ); |
666 | } |
667 | $eventInfo .= $this->getDetailsDialogChat( $page, $registration, $userStatus ); |
668 | |
669 | return Html::rawElement( |
670 | 'div', |
671 | [ 'class' => 'ext-campaignevents-detailsdialog-eventinfo-container' ], |
672 | $eventInfo |
673 | ); |
674 | } |
675 | |
676 | /** |
677 | * @param ExistingEventRegistration $registration |
678 | * @return string |
679 | */ |
680 | private function getDetailsDialogDates( ExistingEventRegistration $registration ): string { |
681 | $formattedStart = $this->eventTimeFormatter->formatStart( $registration, $this->language, $this->viewingUser ); |
682 | $formattedEnd = $this->eventTimeFormatter->formatEnd( $registration, $this->language, $this->viewingUser ); |
683 | $datesMsg = $this->msgFormatter->format( |
684 | MessageValue::new( 'campaignevents-eventpage-dialog-dates' )->params( |
685 | $formattedStart->getTimeAndDate(), |
686 | $formattedStart->getDate(), |
687 | $formattedStart->getTime(), |
688 | $formattedEnd->getTimeAndDate(), |
689 | $formattedEnd->getDate(), |
690 | $formattedEnd->getTime() |
691 | ) |
692 | ); |
693 | $formattedTimezone = EventTimeFormatter::wrapTimeZoneForConversion( |
694 | $this->eventTimeFormatter->formatTimezone( $registration, $this->viewingUser ) |
695 | ); |
696 | // XXX Can't use $msgFormatter due to parse() |
697 | $timezoneMsg = $this->out->msg( 'campaignevents-eventpage-dialog-timezone' ) |
698 | ->params( $formattedTimezone ) |
699 | ->parse(); |
700 | return $this->makeDetailsDialogSection( |
701 | 'clock', |
702 | [ |
703 | EventTimeFormatter::wrapRangeForConversion( $registration, $datesMsg ), |
704 | ( new Tag( 'div' ) )->appendContent( new HtmlSnippet( $timezoneMsg ) ) |
705 | ], |
706 | $this->msgFormatter->format( |
707 | MessageValue::new( 'campaignevents-eventpage-dialog-dates-label' ) |
708 | ), |
709 | '', |
710 | [ 'ext-campaignevents-eventpage-detailsdialog-time' ] |
711 | ); |
712 | } |
713 | |
714 | private function getDetailsDialogLocation( |
715 | ProperPageIdentity $page, |
716 | ExistingEventRegistration $registration, |
717 | int $organizersCount, |
718 | int $userStatus |
719 | ): string { |
720 | $locationElements = []; |
721 | $onlineLocationElements = []; |
722 | if ( $registration->getMeetingType() & EventRegistration::MEETING_TYPE_ONLINE ) { |
723 | $onlineLocationElements[] = ( new Tag( 'h4' ) ) |
724 | ->addClasses( [ 'ext-campaignevents-eventpage-detailsdialog-location-header' ] ) |
725 | ->appendContent( |
726 | $this->msgFormatter->format( |
727 | MessageValue::new( 'campaignevents-eventpage-dialog-online-label' ) |
728 | ) ); |
729 | $meetingURL = $registration->getMeetingURL(); |
730 | if ( $meetingURL === null ) { |
731 | $linkContent = $this->msgFormatter->format( |
732 | MessageValue::new( 'campaignevents-eventpage-dialog-link-not-available' ) |
733 | ->numParams( $organizersCount ) |
734 | ); |
735 | } elseif ( |
736 | $userStatus === self::USER_STATUS_ORGANIZER || |
737 | $userStatus === self::USER_STATUS_PARTICIPANT_CAN_UNREGISTER |
738 | ) { |
739 | $linkContent = new HtmlSnippet( |
740 | $this->linkRenderer->makeExternalLink( $meetingURL, $meetingURL, $page ) |
741 | ); |
742 | } elseif ( $userStatus === self::USER_STATUS_CAN_REGISTER ) { |
743 | $linkContent = $this->msgFormatter->format( |
744 | MessageValue::new( 'campaignevents-eventpage-dialog-link-register' ) |
745 | ); |
746 | } elseif ( $userStatus === self::USER_STATUS_BLOCKED ) { |
747 | $linkContent = $this->msgFormatter->format( |
748 | MessageValue::new( 'campaignevents-event-details-sensitive-data-message-blocked-user' ) |
749 | ); |
750 | } elseif ( |
751 | $userStatus === self::USER_STATUS_CANNOT_REGISTER_CLOSED || |
752 | $userStatus === self::USER_STATUS_CANNOT_REGISTER_ENDED |
753 | ) { |
754 | $linkContent = ''; |
755 | } else { |
756 | throw new LogicException( "Unexpected user status $userStatus" ); |
757 | } |
758 | $onlineLocationElements[] = ( new Tag( 'p' ) )->appendContent( $linkContent ); |
759 | } |
760 | if ( $registration->getMeetingType() & EventRegistration::MEETING_TYPE_IN_PERSON ) { |
761 | $rawAddress = $registration->getMeetingAddress(); |
762 | $rawCountry = $registration->getMeetingCountry(); |
763 | $addressElement = new Tag( 'p' ); |
764 | $addressElement->addClasses( [ 'ext-campaignevents-eventpage-details-address' ] ); |
765 | if ( $rawAddress || $rawCountry ) { |
766 | $address = $rawAddress . "\n" . $rawCountry; |
767 | $addressElement->setAttributes( [ |
768 | 'dir' => Utils::guessStringDirection( $address ) |
769 | ] ); |
770 | $addressElement->appendContent( $address ); |
771 | } else { |
772 | $addressElement->appendContent( $this->msgFormatter->format( |
773 | MessageValue::new( 'campaignevents-eventpage-dialog-venue-not-available' ) |
774 | ->numParams( $organizersCount ) |
775 | ) ); |
776 | } |
777 | if ( $onlineLocationElements ) { |
778 | $inPersonLabel = ( new Tag( 'h4' ) ) |
779 | ->addClasses( [ 'ext-campaignevents-eventpage-detailsdialog-location-header' ] ) |
780 | ->appendContent( $this->msgFormatter->format( |
781 | MessageValue::new( 'campaignevents-eventpage-dialog-in-person-label' ) |
782 | ) ); |
783 | $locationElements[] = $inPersonLabel; |
784 | $locationElements[] = $addressElement; |
785 | $locationElements = array_merge( $locationElements, $onlineLocationElements ); |
786 | } else { |
787 | $locationElements[] = $addressElement; |
788 | } |
789 | } else { |
790 | $locationElements = array_merge( $locationElements, $onlineLocationElements ); |
791 | } |
792 | return $this->makeDetailsDialogSection( |
793 | 'mapPin', |
794 | $locationElements, |
795 | $this->msgFormatter->format( |
796 | MessageValue::new( 'campaignevents-eventpage-dialog-location-label' ) |
797 | ) |
798 | ); |
799 | } |
800 | |
801 | private function getDetailsDialogChat( |
802 | ProperPageIdentity $page, |
803 | ExistingEventRegistration $registration, |
804 | int $userStatus |
805 | ): string { |
806 | $chatURL = $registration->getChatURL(); |
807 | if ( $chatURL === null ) { |
808 | $chatURLContent = $this->msgFormatter->format( |
809 | MessageValue::new( 'campaignevents-eventpage-dialog-chat-not-available' ) |
810 | ); |
811 | } elseif ( |
812 | $userStatus === self::USER_STATUS_ORGANIZER || |
813 | $userStatus === self::USER_STATUS_PARTICIPANT_CAN_UNREGISTER |
814 | ) { |
815 | $chatURLContent = new HtmlSnippet( |
816 | $this->linkRenderer->makeExternalLink( $chatURL, $chatURL, $page ) |
817 | ); |
818 | } elseif ( $userStatus === self::USER_STATUS_CAN_REGISTER ) { |
819 | $chatURLContent = $this->msgFormatter->format( |
820 | MessageValue::new( 'campaignevents-eventpage-dialog-chat-register' ) |
821 | ); |
822 | } elseif ( |
823 | $userStatus === self::USER_STATUS_BLOCKED |
824 | ) { |
825 | $chatURLContent = $this->msgFormatter->format( |
826 | MessageValue::new( 'campaignevents-event-details-sensitive-data-message-blocked-user' ) |
827 | ); |
828 | } elseif ( |
829 | $userStatus === self::USER_STATUS_CANNOT_REGISTER_CLOSED || |
830 | $userStatus === self::USER_STATUS_CANNOT_REGISTER_ENDED |
831 | ) { |
832 | $chatURLContent = ''; |
833 | } else { |
834 | throw new LogicException( "Unexpected user status $userStatus" ); |
835 | } |
836 | |
837 | if ( $chatURLContent ) { |
838 | return $this->makeDetailsDialogSection( |
839 | 'speechBubbles', |
840 | $chatURLContent, |
841 | $this->msgFormatter->format( |
842 | MessageValue::new( 'campaignevents-eventpage-dialog-chat-label' ) |
843 | ) |
844 | ); |
845 | } |
846 | return ''; |
847 | } |
848 | |
849 | /** |
850 | * @param int $eventID |
851 | * @param Participant|null $participant |
852 | * @param ExistingEventRegistration $registration |
853 | * @return string |
854 | */ |
855 | private function getDetailsDialogParticipants( |
856 | int $eventID, |
857 | ?Participant $participant, |
858 | ExistingEventRegistration $registration |
859 | ): string { |
860 | $showPrivateParticipants = $this->permissionChecker->userCanViewPrivateParticipants( |
861 | $this->authority, |
862 | $registration |
863 | ); |
864 | $participantsCount = $this->participantsStore->getFullParticipantCountForEvent( $eventID ); |
865 | $privateCount = $this->participantsStore->getPrivateParticipantCountForEvent( $eventID ); |
866 | $participantsList = $this->getParticipantRows( |
867 | $eventID, |
868 | $participant, |
869 | $showPrivateParticipants |
870 | ); |
871 | $participantsFooter = ''; |
872 | if ( self::PARTICIPANTS_LIMIT < $participantsCount ) { |
873 | $participantsFooter = $this->getParticipantFooter( $eventID ); |
874 | } |
875 | |
876 | $privateCountFooter = ''; |
877 | if ( $privateCount > 0 ) { |
878 | $privateCountFooter = new Tag(); |
879 | $privateCountFooter->addClasses( [ |
880 | 'ext-campaignevents-detailsdialog-private-participants-footer' |
881 | ] ); |
882 | $privateCountIcon = new IconWidget( [ |
883 | 'icon' => 'lock' |
884 | ] ); |
885 | $privateCountText = ( new Tag( 'span' ) ) |
886 | ->addClasses( [ 'ext-campaignevents-detailsdialog-private-participants-footer-text' ] ); |
887 | $privateCountText->appendContent( |
888 | $this->msgFormatter->format( |
889 | MessageValue::new( 'campaignevents-eventpage-dialog-participants-private' ) |
890 | ->numParams( $privateCount ) |
891 | ) |
892 | ); |
893 | |
894 | $privateCountFooter->appendContent( [ $privateCountIcon, $privateCountText ] ); |
895 | } |
896 | |
897 | return $this->makeDetailsDialogSection( |
898 | 'userGroup', |
899 | [ $participantsList ?: '', $participantsFooter ], |
900 | $this->msgFormatter->format( |
901 | MessageValue::new( 'campaignevents-eventpage-dialog-participants' ) |
902 | ->numParams( $participantsCount ) |
903 | ), |
904 | $privateCountFooter |
905 | ); |
906 | } |
907 | |
908 | /** |
909 | * @param int $eventID |
910 | * @param Participant|null $curUserParticipant |
911 | * @param bool $showPrivateParticipants |
912 | * |
913 | * @return Tag|null |
914 | */ |
915 | private function getParticipantRows( |
916 | int $eventID, |
917 | ?Participant $curUserParticipant, |
918 | bool $showPrivateParticipants |
919 | ): ?Tag { |
920 | $participantsList = ( new Tag( 'ul' ) ) |
921 | ->addClasses( [ 'ext-campaignevents-detailsdialog-participants-list' ] ); |
922 | $partialParticipants = $this->participantsStore->getEventParticipants( |
923 | $eventID, |
924 | $curUserParticipant ? |
925 | self::PARTICIPANTS_LIMIT - 1 : |
926 | self::PARTICIPANTS_LIMIT, |
927 | null, |
928 | null, |
929 | null, |
930 | $showPrivateParticipants, |
931 | $curUserParticipant ? [ $curUserParticipant->getUser()->getCentralID() ] : null |
932 | ); |
933 | |
934 | if ( !$curUserParticipant && !$partialParticipants ) { |
935 | return null; |
936 | } |
937 | |
938 | if ( $curUserParticipant ) { |
939 | $participantsList->appendContent( |
940 | $this->getParticipantRow( $curUserParticipant ) |
941 | ); |
942 | } |
943 | foreach ( $partialParticipants as $participant ) { |
944 | $participantsList->appendContent( |
945 | $this->getParticipantRow( $participant ) |
946 | ); |
947 | } |
948 | return $participantsList; |
949 | } |
950 | |
951 | /** |
952 | * Returns the "action" element for the header (that are also cloned into the popup). This can be a button for |
953 | * managing the event, or one to register for it. Or it can be a widget informing the user that they are already |
954 | * registered, with a button to unregister. There can also be no element if the user is not allowed to register. |
955 | * |
956 | * @param int $eventID |
957 | * @param int $userStatus |
958 | * @return Element|null |
959 | */ |
960 | private function getActionElement( int $eventID, int $userStatus ): ?Element { |
961 | if ( $userStatus === self::USER_STATUS_BLOCKED ) { |
962 | return null; |
963 | } |
964 | |
965 | if ( |
966 | $userStatus === self::USER_STATUS_CANNOT_REGISTER_CLOSED || |
967 | $userStatus === self::USER_STATUS_CANNOT_REGISTER_ENDED |
968 | ) { |
969 | $msgKey = $userStatus === self::USER_STATUS_CANNOT_REGISTER_CLOSED |
970 | ? 'campaignevents-eventpage-btn-registration-closed' |
971 | : 'campaignevents-eventpage-btn-event-ended'; |
972 | return new ButtonWidget( [ |
973 | 'disabled' => true, |
974 | 'label' => $this->msgFormatter->format( MessageValue::new( $msgKey ) ), |
975 | 'classes' => [ |
976 | 'ext-campaignevents-eventpage-action-element' |
977 | ], |
978 | ] ); |
979 | } |
980 | |
981 | if ( $userStatus === self::USER_STATUS_ORGANIZER ) { |
982 | return new ButtonWidget( [ |
983 | 'flags' => [ 'progressive' ], |
984 | 'label' => $this->msgFormatter->format( MessageValue::new( 'campaignevents-eventpage-btn-manage' ) ), |
985 | 'classes' => [ |
986 | 'ext-campaignevents-eventpage-action-element', |
987 | ], |
988 | 'href' => SpecialPage::getTitleFor( |
989 | SpecialEventDetails::PAGE_NAME, |
990 | (string)$eventID |
991 | )->getLocalURL(), |
992 | ] ); |
993 | } |
994 | |
995 | if ( $userStatus === self::USER_STATUS_PARTICIPANT_CAN_UNREGISTER ) { |
996 | $unregisterURL = SpecialPage::getTitleFor( |
997 | SpecialCancelEventRegistration::PAGE_NAME, |
998 | (string)$eventID |
999 | )->getLocalURL(); |
1000 | |
1001 | // Note that this will be replaced with a ButtonMenuSelectWidget in JS. |
1002 | return new HorizontalLayout( [ |
1003 | 'items' => [ |
1004 | new ButtonWidget( [ |
1005 | 'flags' => [ 'progressive' ], |
1006 | 'label' => $this->msgFormatter->format( |
1007 | MessageValue::new( 'campaignevents-eventpage-btn-edit' ) |
1008 | ), |
1009 | 'href' => SpecialPage::getTitleFor( SpecialRegisterForEvent::PAGE_NAME, (string)$eventID ) |
1010 | ->getLocalURL(), |
1011 | ] ), |
1012 | new ButtonWidget( [ |
1013 | 'flags' => [ 'destructive' ], |
1014 | 'label' => $this->msgFormatter->format( |
1015 | MessageValue::new( 'campaignevents-eventpage-btn-cancel' ) |
1016 | ), |
1017 | 'href' => $unregisterURL, |
1018 | ] ) |
1019 | ], |
1020 | 'classes' => [ |
1021 | 'ext-campaignevents-eventpage-manage-registration-layout', |
1022 | 'ext-campaignevents-eventpage-action-element' |
1023 | ] |
1024 | ] ); |
1025 | } |
1026 | |
1027 | if ( $userStatus === self::USER_STATUS_CAN_REGISTER ) { |
1028 | return new ButtonWidget( [ |
1029 | 'flags' => [ 'primary', 'progressive' ], |
1030 | 'label' => $this->msgFormatter->format( MessageValue::new( 'campaignevents-eventpage-btn-register' ) ), |
1031 | 'classes' => [ |
1032 | 'ext-campaignevents-eventpage-register-btn', |
1033 | 'ext-campaignevents-eventpage-action-element' |
1034 | ], |
1035 | 'href' => SpecialPage::getTitleFor( SpecialRegisterForEvent::PAGE_NAME, (string)$eventID ) |
1036 | ->getLocalURL(), |
1037 | ] ); |
1038 | } |
1039 | throw new LogicException( "Unexpected user status $userStatus" ); |
1040 | } |
1041 | |
1042 | /** |
1043 | * @param ExistingEventRegistration $event |
1044 | * @param CentralUser|null $centralUser Corresponding to $this->authority, if it exists |
1045 | * @param Participant|null $participant For $centralUser, if they're a participant |
1046 | * @return int One of the SELF::USER_STATUS_* constants |
1047 | */ |
1048 | private function getUserStatus( |
1049 | ExistingEventRegistration $event, |
1050 | ?CentralUser $centralUser, |
1051 | ?Participant $participant |
1052 | ): int { |
1053 | // Do not check user blocks or other user-dependent conditions for logged-out users, so that we can serve the |
1054 | // same (cached) version of the page to everyone. Also, even if the IP is blocked, the user might have an |
1055 | // account that they can log into, so showing the button is fine. |
1056 | if ( $centralUser ) { |
1057 | if ( $this->authority->isSitewideBlocked() ) { |
1058 | return self::USER_STATUS_BLOCKED; |
1059 | } |
1060 | |
1061 | if ( $this->organizersStore->isEventOrganizer( $event->getID(), $centralUser ) ) { |
1062 | return self::USER_STATUS_ORGANIZER; |
1063 | } |
1064 | |
1065 | if ( $participant ) { |
1066 | $checkUnregistrationAllowedVal = UnregisterParticipantCommand::checkIsUnregistrationAllowed( $event ); |
1067 | switch ( $checkUnregistrationAllowedVal->getValue() ) { |
1068 | case UnregisterParticipantCommand::CANNOT_UNREGISTER_DELETED: |
1069 | throw new UnexpectedValueException( "Registration should not be deleted at this point." ); |
1070 | case UnregisterParticipantCommand::CAN_UNREGISTER: |
1071 | $this->participantIsPublic = !$participant->isPrivateRegistration(); |
1072 | return self::USER_STATUS_PARTICIPANT_CAN_UNREGISTER; |
1073 | default: |
1074 | throw new UnexpectedValueException( "Unexpected value $checkUnregistrationAllowedVal" ); |
1075 | } |
1076 | } |
1077 | } |
1078 | |
1079 | // User is logged-in and not already participating, or logged-out, in which case we'll know better |
1080 | // once they log in. |
1081 | $checkRegistrationAllowedVal = RegisterParticipantCommand::checkIsRegistrationAllowed( |
1082 | $event, |
1083 | RegisterParticipantCommand::REGISTRATION_NEW |
1084 | ); |
1085 | switch ( $checkRegistrationAllowedVal->value ) { |
1086 | case RegisterParticipantCommand::CANNOT_REGISTER_DELETED: |
1087 | throw new UnexpectedValueException( "Registration should not be deleted at this point." ); |
1088 | case RegisterParticipantCommand::CANNOT_REGISTER_ENDED: |
1089 | return self::USER_STATUS_CANNOT_REGISTER_ENDED; |
1090 | case RegisterParticipantCommand::CANNOT_REGISTER_CLOSED: |
1091 | return self::USER_STATUS_CANNOT_REGISTER_CLOSED; |
1092 | case RegisterParticipantCommand::CAN_REGISTER: |
1093 | return self::USER_STATUS_CAN_REGISTER; |
1094 | default: |
1095 | throw new UnexpectedValueException( "Unexpected value $checkRegistrationAllowedVal" ); |
1096 | } |
1097 | } |
1098 | |
1099 | /** |
1100 | * @param int $eventID |
1101 | * @return Tag |
1102 | */ |
1103 | private function getParticipantFooter( int $eventID ): Tag { |
1104 | $viewParticipantsURL = SpecialPage::getTitleFor( SpecialEventDetails::PAGE_NAME, (string)$eventID ) |
1105 | ->getLocalURL( [ 'tab' => SpecialEventDetails::PARTICIPANTS_PANEL ] ); |
1106 | return new ButtonWidget( [ |
1107 | 'framed' => false, |
1108 | 'flags' => [ 'progressive' ], |
1109 | 'label' => $this->msgFormatter->format( |
1110 | MessageValue::new( 'campaignevents-eventpage-dialog-participants-view-list' ) |
1111 | ), |
1112 | 'href' => $viewParticipantsURL, |
1113 | ] ); |
1114 | } |
1115 | |
1116 | /** |
1117 | * @param Participant $participant |
1118 | * @return Tag |
1119 | */ |
1120 | private function getParticipantRow( Participant $participant ): Tag { |
1121 | $usernameElement = new HtmlSnippet( |
1122 | $this->userLinker->generateUserLinkWithFallback( |
1123 | $participant->getUser(), |
1124 | $this->language->getCode() |
1125 | ) |
1126 | ); |
1127 | |
1128 | $tag = ( new Tag( 'li' ) ) |
1129 | ->appendContent( $usernameElement ); |
1130 | |
1131 | if ( $participant->isPrivateRegistration() ) { |
1132 | try { |
1133 | $userName = $this->centralUserLookup->getUserName( $participant->getUser() ); |
1134 | } catch ( CentralUserNotFoundException | HiddenCentralUserException $_ ) { |
1135 | // Hack: use an invalid username to force unspecified gender |
1136 | $userName = '@'; |
1137 | } |
1138 | $labelText = $this->msgFormatter->format( |
1139 | MessageValue::new( 'campaignevents-eventpage-dialog-private-registration-label' ) |
1140 | ->params( $userName ) |
1141 | ); |
1142 | $tag->appendContent( new IconWidget( [ |
1143 | 'icon' => 'lock', |
1144 | 'title' => $labelText, |
1145 | 'label' => $labelText, |
1146 | 'classes' => [ 'ext-campaignevents-event-details-participants-private-icon' ] |
1147 | ] ) |
1148 | ); |
1149 | } |
1150 | |
1151 | return $tag; |
1152 | } |
1153 | |
1154 | /** |
1155 | * @param string $icon |
1156 | * @param string|Tag|array|HtmlSnippet $content |
1157 | * @param string $label |
1158 | * @param string|Tag|array $footer |
1159 | * @param string[] $classes |
1160 | * @return string |
1161 | */ |
1162 | private function makeDetailsDialogSection( |
1163 | string $icon, |
1164 | $content, |
1165 | string $label, |
1166 | $footer = '', |
1167 | array $classes = [] |
1168 | ): string { |
1169 | $iconWidget = new IconWidget( [ |
1170 | 'icon' => $icon, |
1171 | 'classes' => [ 'ext-campaignevents-eventpage-detailsdialog-section-icon' ] |
1172 | ] ); |
1173 | $header = ( new Tag( 'h3' ) ) |
1174 | ->appendContent( $iconWidget, ( new Tag( 'span' ) )->appendContent( $label ) ) |
1175 | ->addClasses( [ 'ext-campaignevents-eventpage-detailsdialog-section-header' ] ); |
1176 | |
1177 | $contentTag = ( new Tag( 'div' ) ) |
1178 | ->appendContent( $content ) |
1179 | ->addClasses( [ 'ext-campaignevents-eventpage-detailsdialog-section-content', ...$classes ] ); |
1180 | |
1181 | return (string)( new Tag( 'div' ) ) |
1182 | ->appendContent( $header, $contentTag, $footer ); |
1183 | } |
1184 | |
1185 | /** |
1186 | * @param ExistingEventRegistration $registration |
1187 | * @return string |
1188 | */ |
1189 | private function getDetailsDialogWikis( ExistingEventRegistration $registration ): string { |
1190 | $content = EventFormatter::formatWikis( |
1191 | $registration, |
1192 | $this->msgFormatter, |
1193 | $this->wikiLookup, |
1194 | $this->language, |
1195 | $this->linkRenderer, |
1196 | 'campaignevents-eventpage-all-wikis', |
1197 | 'campaignevents-eventpage-wikis-more', |
1198 | ); |
1199 | return $this->makeDetailsDialogSection( |
1200 | $this->wikiLookup->getWikiIcon( $registration->getWikis() ), |
1201 | $content, |
1202 | $this->msgFormatter->format( |
1203 | MessageValue::new( 'campaignevents-eventpage-dialog-wikis-label' ) |
1204 | ) |
1205 | ); |
1206 | } |
1207 | } |