Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.17% |
107 / 109 |
|
83.33% |
5 / 6 |
CRAP | |
0.00% |
0 / 1 |
ParticipantAnswersStore | |
98.17% |
107 / 109 |
|
83.33% |
5 / 6 |
24 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
replaceParticipantAnswers | |
100.00% |
58 / 58 |
|
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 | ->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 | } |