Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 145
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialRegisterForEvent
0.00% covered (danger)
0.00%
0 / 145
0.00% covered (danger)
0.00%
0 / 8
650
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 / 24
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 / 12
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
1<?php
2
3declare( strict_types=1 );
4
5namespace MediaWiki\Extension\CampaignEvents\Special;
6
7use HTMLForm;
8use MediaWiki\Extension\CampaignEvents\Event\Store\IEventLookup;
9use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsCentralUserLookup;
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\Status\Status;
21use MediaWiki\Utils\MWTimestamp;
22use OOUI\IconWidget;
23
24class 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}