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