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