Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 156 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
SpecialRegisterForEvent | |
0.00% |
0 / 156 |
|
0.00% |
0 / 9 |
812 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getForm | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
getFormFields | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
12 | |||
addParticipantQuestionFields | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
56 | |||
alterForm | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
onSubmit | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
12 | |||
onSuccess | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getShowAlways | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkEventIsValid | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents\Special; |
6 | |
7 | use MediaWiki\Extension\CampaignEvents\Event\Store\IEventLookup; |
8 | use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsCentralUserLookup; |
9 | use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUser; |
10 | use MediaWiki\Extension\CampaignEvents\MWEntity\MWAuthorityProxy; |
11 | use MediaWiki\Extension\CampaignEvents\MWEntity\UserNotGlobalException; |
12 | use MediaWiki\Extension\CampaignEvents\Participants\Participant; |
13 | use MediaWiki\Extension\CampaignEvents\Participants\ParticipantsStore; |
14 | use MediaWiki\Extension\CampaignEvents\Participants\RegisterParticipantCommand; |
15 | use MediaWiki\Extension\CampaignEvents\PolicyMessagesLookup; |
16 | use MediaWiki\Extension\CampaignEvents\Questions\EventQuestionsRegistry; |
17 | use MediaWiki\Extension\CampaignEvents\Questions\InvalidAnswerDataException; |
18 | use MediaWiki\Extension\CampaignEvents\Utils; |
19 | use MediaWiki\Html\Html; |
20 | use MediaWiki\HTMLForm\HTMLForm; |
21 | use MediaWiki\Status\Status; |
22 | use MediaWiki\Utils\MWTimestamp; |
23 | use OOUI\IconWidget; |
24 | use StatusValue; |
25 | |
26 | class SpecialRegisterForEvent extends ChangeRegistrationSpecialPageBase { |
27 | public const PAGE_NAME = 'RegisterForEvent'; |
28 | |
29 | private const QUESTIONS_SECTION_NAME = 'campaignevents-register-questions-label-title'; |
30 | |
31 | private RegisterParticipantCommand $registerParticipantCommand; |
32 | private ParticipantsStore $participantsStore; |
33 | private PolicyMessagesLookup $policyMessagesLookup; |
34 | private EventQuestionsRegistry $eventQuestionsRegistry; |
35 | |
36 | /** |
37 | * @var Participant|null If the user is already registered, this is their Participant record, containing |
38 | * info about their current state. |
39 | */ |
40 | private ?Participant $curParticipantData; |
41 | /** |
42 | * @var bool Whether the user has any aggregated answers for this event. This can be true even if the user is not |
43 | * a participant (if they cancelled their registration after their answers had been aggregated). |
44 | */ |
45 | private bool $hasAggregatedAnswers; |
46 | /** |
47 | * @var bool|null Whether the operation resulted in any data about the participant being modified. |
48 | */ |
49 | private ?bool $modifiedData; |
50 | /** @var bool|null Whether the user is editing their registration, as opposed to registering for the first time */ |
51 | private ?bool $isEdit; |
52 | /** @var array|null IDs of participant questions to show in the form */ |
53 | private ?array $participantQuestionsToShow; |
54 | private ?CentralUser $centralUser; |
55 | |
56 | /** |
57 | * @param IEventLookup $eventLookup |
58 | * @param CampaignsCentralUserLookup $centralUserLookup |
59 | * @param RegisterParticipantCommand $registerParticipantCommand |
60 | * @param ParticipantsStore $participantsStore |
61 | * @param PolicyMessagesLookup $policyMessagesLookup |
62 | * @param EventQuestionsRegistry $eventQuestionsRegistry |
63 | */ |
64 | public function __construct( |
65 | IEventLookup $eventLookup, |
66 | CampaignsCentralUserLookup $centralUserLookup, |
67 | RegisterParticipantCommand $registerParticipantCommand, |
68 | ParticipantsStore $participantsStore, |
69 | PolicyMessagesLookup $policyMessagesLookup, |
70 | EventQuestionsRegistry $eventQuestionsRegistry |
71 | ) { |
72 | parent::__construct( self::PAGE_NAME, $eventLookup, $centralUserLookup ); |
73 | $this->registerParticipantCommand = $registerParticipantCommand; |
74 | $this->participantsStore = $participantsStore; |
75 | $this->policyMessagesLookup = $policyMessagesLookup; |
76 | $this->eventQuestionsRegistry = $eventQuestionsRegistry; |
77 | $this->getOutput()->enableOOUI(); |
78 | $this->getOutput()->addModuleStyles( [ |
79 | 'ext.campaignEvents.specialPages.styles', |
80 | 'oojs-ui.styles.icons-location', |
81 | 'oojs-ui.styles.icons-moderation', |
82 | ] ); |
83 | } |
84 | |
85 | /** |
86 | * @inheritDoc |
87 | */ |
88 | protected function getForm(): HTMLForm { |
89 | if ( $this->centralUser !== null ) { |
90 | $this->hasAggregatedAnswers = $this->participantsStore->userHasAggregatedAnswers( |
91 | $this->event->getID(), |
92 | $this->centralUser |
93 | ); |
94 | } |
95 | |
96 | $this->isEdit = $this->curParticipantData || $this->getRequest()->wasPosted(); |
97 | $enabledQuestions = $this->event->getParticipantQuestions(); |
98 | $curAnswers = $this->curParticipantData ? $this->curParticipantData->getAnswers() : []; |
99 | $this->participantQuestionsToShow = EventQuestionsRegistry::getParticipantQuestionsToShow( |
100 | $enabledQuestions, |
101 | $curAnswers |
102 | ); |
103 | $this->getOutput()->setPageTitleMsg( |
104 | $this->msg( 'campaignevents-event-register-for-event-title', $this->event->getName() ) |
105 | ); |
106 | return parent::getForm(); |
107 | } |
108 | |
109 | /** |
110 | * @inheritDoc |
111 | */ |
112 | protected function getFormFields(): array { |
113 | $publicIcon = |
114 | new IconWidget( [ |
115 | 'icon' => 'globe', |
116 | 'classes' => [ 'ext-campaignevents-registerforevent-icon' ] |
117 | ] ); |
118 | $privateIcon = |
119 | new IconWidget( [ |
120 | 'icon' => 'lock', |
121 | 'classes' => [ 'ext-campaignevents-registerforevent-icon' ] |
122 | ] ); |
123 | |
124 | // Use a fake "top" section to force ordering |
125 | $fields = [ |
126 | 'IsPrivate' => [ |
127 | 'type' => 'radio', |
128 | 'options' => [ |
129 | $this->msg( 'campaignevents-register-confirmation-radio-public' ) . $publicIcon => false, |
130 | $this->msg( 'campaignevents-register-confirmation-radio-private' ) . $privateIcon => true |
131 | ], |
132 | 'default' => $this->curParticipantData ? $this->curParticipantData->isPrivateRegistration() : false, |
133 | 'section' => 'top', |
134 | ] |
135 | ]; |
136 | |
137 | $this->addParticipantQuestionFields( $fields ); |
138 | |
139 | $policyMsg = $this->policyMessagesLookup->getPolicyMessageForRegistration(); |
140 | if ( $policyMsg !== null ) { |
141 | $fields['Policy'] = [ |
142 | 'type' => 'info', |
143 | 'raw' => true, |
144 | 'default' => $this->msg( $policyMsg )->parse(), |
145 | ]; |
146 | } |
147 | |
148 | return $fields; |
149 | } |
150 | |
151 | private function addParticipantQuestionFields( array &$fields ): void { |
152 | if ( $this->hasAggregatedAnswers ) { |
153 | $fields['AnswersAggregated'] = [ |
154 | 'type' => 'info', |
155 | 'default' => Html::element( |
156 | 'strong', |
157 | [], |
158 | $this->msg( 'campaignevents-register-answers-aggregated' )->text() |
159 | ), |
160 | 'raw' => true, |
161 | 'section' => self::QUESTIONS_SECTION_NAME, |
162 | ]; |
163 | } elseif ( !$this->participantQuestionsToShow ) { |
164 | return; |
165 | } else { |
166 | $curAnswers = $this->curParticipantData ? $this->curParticipantData->getAnswers() : []; |
167 | $questionFields = $this->eventQuestionsRegistry->getQuestionsForHTMLForm( |
168 | $this->participantQuestionsToShow, |
169 | $curAnswers |
170 | ); |
171 | // XXX: This is affected by the following bug. Say we have an answer for question 1, and the organizer has |
172 | // removed that question. We would show it here initially, which is correct. But then, if the user blanks |
173 | // out the field and submit, it will still be shown after submission, even though it will no longer be |
174 | // possible to submit a different value. This seems non-trivial to fix because this code runs before |
175 | // onSubmit(), i.e. before we know what the updated user answers will be after form submission. |
176 | $questionFields = array_map( |
177 | static fn ( $fieldDescriptor ) => |
178 | [ 'section' => self::QUESTIONS_SECTION_NAME ] + $fieldDescriptor, |
179 | $questionFields |
180 | ); |
181 | $fields += $questionFields; |
182 | } |
183 | |
184 | $retentionMsg = $this->msg( 'campaignevents-register-retention-base' )->escaped(); |
185 | if ( $this->curParticipantData ) { |
186 | $plannedAggregationTS = Utils::getAnswerAggregationTimestamp( $this->curParticipantData, $this->event ); |
187 | if ( $plannedAggregationTS !== null ) { |
188 | $timeRemaining = (int)$plannedAggregationTS - (int)MWTimestamp::now( TS_UNIX ); |
189 | if ( $timeRemaining < 60 * 60 * 24 ) { |
190 | $additionalRetentionMsg = $this->msg( 'campaignevents-register-retention-hours' )->parse(); |
191 | } else { |
192 | $remainingDays = intdiv( $timeRemaining, 60 * 60 * 24 ); |
193 | $additionalRetentionMsg = $this->msg( 'campaignevents-register-retention-days' ) |
194 | ->numParams( $remainingDays ) |
195 | ->parse(); |
196 | } |
197 | $retentionMsg .= $this->msg( 'word-separator' )->escaped() . $additionalRetentionMsg; |
198 | } |
199 | } |
200 | $fields['DataRetentionInfo'] = [ |
201 | 'type' => 'info', |
202 | 'raw' => true, |
203 | 'default' => $retentionMsg, |
204 | 'section' => 'campaignevents-register-retention-title', |
205 | ]; |
206 | } |
207 | |
208 | /** |
209 | * @inheritDoc |
210 | */ |
211 | protected function alterForm( HTMLForm $form ): void { |
212 | if ( $this->isEdit ) { |
213 | $form->setWrapperLegendMsg( 'campaignevents-register-edit-legend' ); |
214 | $form->setSubmitTextMsg( 'campaignevents-register-edit-btn' ); |
215 | } else { |
216 | $form->setWrapperLegendMsg( 'campaignevents-register-confirmation-top' ); |
217 | $form->setSubmitTextMsg( 'campaignevents-register-confirmation-btn' ); |
218 | } |
219 | |
220 | if ( $this->participantQuestionsToShow ) { |
221 | $questionsHeader = Html::rawElement( |
222 | 'div', |
223 | [ 'class' => 'ext-campaignevents-registerforevent-participant-questions-info-subtitle' ], |
224 | $this->msg( 'campaignevents-register-questions-label-subtitle' )->parseAsBlock() |
225 | ); |
226 | $form->addHeaderHtml( $questionsHeader, self::QUESTIONS_SECTION_NAME ); |
227 | } |
228 | $form->setId( 'ext-campaignevents-registerforevent-form' ); |
229 | } |
230 | |
231 | /** |
232 | * @inheritDoc |
233 | */ |
234 | public function onSubmit( array $data ) { |
235 | $privateFlag = $data['IsPrivate'] ? |
236 | RegisterParticipantCommand::REGISTRATION_PRIVATE : |
237 | RegisterParticipantCommand::REGISTRATION_PUBLIC; |
238 | |
239 | try { |
240 | $answers = $this->eventQuestionsRegistry->extractUserAnswersHTMLForm( |
241 | $data, |
242 | $this->participantQuestionsToShow |
243 | ); |
244 | } catch ( InvalidAnswerDataException $e ) { |
245 | // Should never happen unless the user messes up with the form, so don't bother making this too pretty. |
246 | return Status::newFatal( 'campaignevents-register-invalid-answer', $e->getQuestionName() ); |
247 | } |
248 | |
249 | $status = $this->registerParticipantCommand->registerIfAllowed( |
250 | $this->event, |
251 | new MWAuthorityProxy( $this->getAuthority() ), |
252 | $privateFlag, |
253 | $answers |
254 | ); |
255 | $this->modifiedData = $status->getValue(); |
256 | return Status::wrap( $status ); |
257 | } |
258 | |
259 | /** |
260 | * @inheritDoc |
261 | */ |
262 | public function onSuccess(): void { |
263 | if ( $this->modifiedData === false ) { |
264 | // No change to the previous data, don't show a success message. |
265 | // TODO We might want to explicitly inform the user that nothing changed. |
266 | return; |
267 | } |
268 | // Note: we can't use isEdit here because that's computed before this method is called, |
269 | // and it'll always be true at this point. |
270 | $successMsg = $this->curParticipantData |
271 | ? 'campaignevents-register-success-edit' |
272 | : 'campaignevents-register-success'; |
273 | $this->getOutput()->prependHTML( Html::successBox( |
274 | $this->msg( $successMsg )->escaped() |
275 | ) ); |
276 | } |
277 | |
278 | /** |
279 | * @inheritDoc |
280 | */ |
281 | protected function getShowAlways(): bool { |
282 | return true; |
283 | } |
284 | |
285 | /** |
286 | * @inheritDoc |
287 | */ |
288 | public function checkEventIsValid(): StatusValue { |
289 | try { |
290 | $this->centralUser = $this->centralUserLookup->newFromAuthority( |
291 | new MWAuthorityProxy( $this->getAuthority() ) |
292 | ); |
293 | $this->curParticipantData = $this->participantsStore->getEventParticipant( |
294 | $this->event->getID(), |
295 | $this->centralUser, |
296 | true |
297 | ); |
298 | } catch ( UserNotGlobalException $_ ) { |
299 | $this->centralUser = null; |
300 | $this->curParticipantData = null; |
301 | $this->hasAggregatedAnswers = false; |
302 | } |
303 | return RegisterParticipantCommand::checkIsRegistrationAllowed( |
304 | $this->event, |
305 | $this->curParticipantData ? |
306 | RegisterParticipantCommand::REGISTRATION_EDIT : |
307 | RegisterParticipantCommand::REGISTRATION_NEW |
308 | ); |
309 | } |
310 | } |