Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.17% covered (success)
98.17%
107 / 109
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParticipantAnswersStore
98.17% covered (success)
98.17%
107 / 109
83.33% covered (warning)
83.33%
5 / 6
24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replaceParticipantAnswers
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
1 / 1
10
 deleteAllAnswers
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
7.07
 getParticipantAnswers
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getParticipantAnswersMulti
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
4
 eventHasAnswers
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare( strict_types=1 );
4
5namespace MediaWiki\Extension\CampaignEvents\Questions;
6
7use InvalidArgumentException;
8use MediaWiki\Extension\CampaignEvents\Database\CampaignsDatabaseHelper;
9use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUser;
10
11class ParticipantAnswersStore {
12    public const SERVICE_NAME = 'CampaignEventsParticipantAnswersStore';
13
14    private CampaignsDatabaseHelper $dbHelper;
15
16    /**
17     * @param CampaignsDatabaseHelper $dbHelper
18     */
19    public function __construct( CampaignsDatabaseHelper $dbHelper ) {
20        $this->dbHelper = $dbHelper;
21    }
22
23    /**
24     * @param int $eventID
25     * @param CentralUser $participant
26     * @param Answer[] $answers
27     * @return bool Whether any stored answers were modified
28     */
29    public function replaceParticipantAnswers( int $eventID, CentralUser $participant, array $answers ): bool {
30        $userID = $participant->getCentralID();
31        $dbw = $this->dbHelper->getDBConnection( DB_PRIMARY );
32        $currentAnswers = $dbw->newSelectQueryBuilder()
33            ->select( '*' )
34            ->from( 'ce_question_answers' )
35            ->where( [
36                'ceqa_event_id' => $eventID,
37                'ceqa_user_id' => $userID,
38            ] )
39            ->forUpdate()
40            ->caller( __METHOD__ )
41            ->fetchResultSet();
42        $newQuestionIDs = array_map( static fn ( Answer $a ): int => $a->getQuestionDBID(), $answers );
43        $currentAnswersByID = [];
44        $rowIDsToRemove = [];
45        foreach ( $currentAnswers as $row ) {
46            $questionID = (int)$row->ceqa_question_id;
47            $currentAnswersByID[$questionID] = [
48                $row->ceqa_answer_option !== null ? (int)$row->ceqa_answer_option : $row->ceqa_answer_option,
49                $row->ceqa_answer_text
50            ];
51            if ( !in_array( $questionID, $newQuestionIDs, true ) ) {
52                $rowIDsToRemove[] = $row->ceqa_id;
53            }
54        }
55        if ( $rowIDsToRemove ) {
56            $dbw->newDeleteQueryBuilder()
57                ->deleteFrom( 'ce_question_answers' )
58                ->where( [ 'ceqa_id' => $rowIDsToRemove ] )
59                ->caller( __METHOD__ )
60                ->execute();
61        }
62
63        $newRows = [];
64        foreach ( $answers as $answer ) {
65            $questionID = $answer->getQuestionDBID();
66            $option = $answer->getOption();
67            $text = $answer->getText();
68
69            $curAnswer = $currentAnswersByID[$questionID] ?? null;
70            if ( $curAnswer && $curAnswer[0] === $option && $curAnswer[1] === $text ) {
71                // No change.
72                continue;
73            }
74
75            $newRows[] = [
76                'ceqa_event_id' => $eventID,
77                'ceqa_user_id' => $userID,
78                'ceqa_question_id' => $questionID,
79                'ceqa_answer_option' => $answer->getOption(),
80                'ceqa_answer_text' => $answer->getText(),
81            ];
82        }
83
84        if ( !$newRows ) {
85            return $rowIDsToRemove !== [];
86        }
87
88        $dbw->newInsertQueryBuilder()
89            ->insertInto( 'ce_question_answers' )
90            ->rows( $newRows )
91            ->onDuplicateKeyUpdate()
92            ->uniqueIndexFields( [ 'ceqa_event_id', 'ceqa_user_id', 'ceqa_question_id' ] )
93            ->set( [
94                'ceqa_answer_option = ' . $dbw->buildExcludedValue( 'ceqa_answer_option' ),
95                'ceqa_answer_text = ' . $dbw->buildExcludedValue( 'ceqa_answer_text' ),
96            ] )
97            ->caller( __METHOD__ )
98            ->execute();
99        return true;
100    }
101
102    /**
103     * Deletes all the answers provided by the given users.
104     *
105     * @param int $eventID
106     * @param CentralUser[]|null $participants Must never be empty, pass null to remove answers for all participants.
107     * @param bool $invertSelection Whether the selection of $participants should be inverted, i.e., only answers
108     *  of users not in $participants will be removed. If true, $participants must not be null.
109     * @return void
110     */
111    public function deleteAllAnswers( int $eventID, ?array $participants, bool $invertSelection = false ): void {
112        if ( $participants === null && $invertSelection ) {
113            throw new InvalidArgumentException( 'Cannot use $invertSelection when removing all answers' );
114        }
115        if ( is_array( $participants ) && !$participants ) {
116            throw new InvalidArgumentException( '$participants cannot be the empty array' );
117        }
118
119        $dbw = $this->dbHelper->getDBConnection( DB_PRIMARY );
120        $where = [
121            'ceqa_event_id' => $eventID,
122        ];
123        if ( $participants !== null ) {
124            $userIDs = array_map( static fn ( CentralUser $u ): int => $u->getCentralID(), $participants );
125            if ( $invertSelection ) {
126                $where[] = $dbw->expr( 'ceqa_user_id', '!=', $userIDs );
127            } else {
128                $where['ceqa_user_id'] = $userIDs;
129            }
130        }
131        $dbw->newDeleteQueryBuilder()
132            ->deleteFrom( 'ce_question_answers' )
133            ->where( $where )
134            ->caller( __METHOD__ )
135            ->execute();
136    }
137
138    /**
139     * @param int $eventID
140     * @param CentralUser $participant
141     * @return Answer[]
142     */
143    public function getParticipantAnswers( int $eventID, CentralUser $participant ): array {
144        $userID = $participant->getCentralID();
145        return $this->getParticipantAnswersMulti( $eventID, [ $participant ] )[$userID];
146    }
147
148    /**
149     * Returns the answers of multiple participants.
150     *
151     * @param int $eventID
152     * @param CentralUser[] $participants
153     * @return array<int,Answer[]> Keys are user IDs. This is guaranteed to contain an entry for each
154     * user ID in $participants.
155     */
156    public function getParticipantAnswersMulti( int $eventID, array $participants ): array {
157        if ( !$participants ) {
158            return [];
159        }
160        $participantIDs = array_map( static fn ( CentralUser $u ) => $u->getCentralID(), $participants );
161        $dbr = $this->dbHelper->getDBConnection( DB_REPLICA );
162        $res = $dbr->newSelectQueryBuilder()
163            ->select( '*' )
164            ->from( 'ce_question_answers' )
165            ->where( [
166                'ceqa_event_id' => $eventID,
167                'ceqa_user_id' => $participantIDs
168            ] )
169            ->caller( __METHOD__ )
170            ->fetchResultSet();
171        $answersByUser = array_fill_keys( $participantIDs, [] );
172        foreach ( $res as $row ) {
173            $userID = (int)$row->ceqa_user_id;
174            $answersByUser[$userID][] = new Answer(
175                (int)$row->ceqa_question_id,
176                $row->ceqa_answer_option !== null ? (int)$row->ceqa_answer_option : $row->ceqa_answer_option,
177                $row->ceqa_answer_text
178            );
179        }
180        return $answersByUser;
181    }
182
183    /**
184     * Returns whether the given event has any answers.
185     *
186     * @param int $eventID
187     * @return bool
188     */
189    public function eventHasAnswers( int $eventID ): bool {
190        $dbr = $this->dbHelper->getDBConnection( DB_REPLICA );
191        $res = $dbr->newSelectQueryBuilder()
192            ->select( '1' )
193            ->from( 'ce_question_answers' )
194            ->where( [ 'ceqa_event_id' => $eventID ] )
195            ->caller( __METHOD__ )
196            ->fetchRow();
197        return $res !== false;
198    }
199}