Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 156
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialRegisterForEvent
0.00% covered (danger)
0.00%
0 / 156
0.00% covered (danger)
0.00%
0 / 9
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getForm
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 getFormFields
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
 addParticipantQuestionFields
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
56
 alterForm
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 onSubmit
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 onSuccess
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getShowAlways
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkEventIsValid
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare( strict_types=1 );
4
5namespace MediaWiki\Extension\CampaignEvents\Special;
6
7use MediaWiki\Extension\CampaignEvents\Event\Store\IEventLookup;
8use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsCentralUserLookup;
9use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUser;
10use MediaWiki\Extension\CampaignEvents\MWEntity\MWAuthorityProxy;
11use MediaWiki\Extension\CampaignEvents\MWEntity\UserNotGlobalException;
12use MediaWiki\Extension\CampaignEvents\Participants\Participant;
13use MediaWiki\Extension\CampaignEvents\Participants\ParticipantsStore;
14use MediaWiki\Extension\CampaignEvents\Participants\RegisterParticipantCommand;
15use MediaWiki\Extension\CampaignEvents\PolicyMessagesLookup;
16use MediaWiki\Extension\CampaignEvents\Questions\EventQuestionsRegistry;
17use MediaWiki\Extension\CampaignEvents\Questions\InvalidAnswerDataException;
18use MediaWiki\Extension\CampaignEvents\Utils;
19use MediaWiki\Html\Html;
20use MediaWiki\HTMLForm\HTMLForm;
21use MediaWiki\Status\Status;
22use MediaWiki\Utils\MWTimestamp;
23use OOUI\IconWidget;
24use StatusValue;
25
26class 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}