Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.15% |
106 / 108 |
|
83.33% |
5 / 6 |
CRAP | |
0.00% |
0 / 1 |
ParticipantAnswersStore | |
98.15% |
106 / 108 |
|
83.33% |
5 / 6 |
24 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
replaceParticipantAnswers | |
100.00% |
57 / 57 |
|
100.00% |
1 / 1 |
10 | |||
deleteAllAnswers | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
7.07 | |||
getParticipantAnswers | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getParticipantAnswersMulti | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
4 | |||
eventHasAnswers | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents\Questions; |
6 | |
7 | use InvalidArgumentException; |
8 | use MediaWiki\Extension\CampaignEvents\Database\CampaignsDatabaseHelper; |
9 | use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUser; |
10 | |
11 | class 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 | } |